Files
Rayyan c3ce5c2f22
Deploy / deploy (push) Successful in 1m4s
bot telegram debt init
2026-09-03 00:35:24 +07:00

23 KiB

RayLab Bot Debt API

Base path: /api/bot/debt/

This document is the API contract for the Debt Management endpoints implemented in RayLab Core. The contents are generated from the actual code in the repository and reflect the real request/response shapes and behavior. Do not assume fields or endpoints that are not present in code.

Authentication

  • All endpoints under /api/bot/debt/* require JWT authentication via Authorization: Bearer (JwtAuthGuard).
  • CurrentUserGuard maps the verified identity to internal User (via SyncIdentityHandler for external identities).
  • All responses are wrapped as { success: true, data: ... } on success. Errors are returned via the global exception filter as { success: false, error: { code, message } }.

NOTE: See the "Authentication Gap" section below for how Telegram Bots should obtain JWTs.


CONTRACT FORMAT (per-endpoint) Method Path Authentication Purpose Request (example JSON) Response success (example JSON) Possible errors (HTTP code + example) Notes


  1. POST /api/bot/debt/users/sync Authentication: Bearer JWT required Purpose: Sync a Telegram identity to an internal RayLab user via SyncIdentityHandler and return internal user summary. Request DTO: SyncUserDto Example request JSON: { "telegramUserId": "123456789", "displayName": "Rayyan" } Response success example: { "success": true, "data": { "id": "", "username": "rayyan", "name": "rayyan" } } Possible errors:
  • 400 Bad Request: { success: false, error: { code: 'BAD_REQUEST', message: 'telegramUserId required' } }
  • 401 / 403 may be returned by JwtAuthGuard/CurrentUserGuard depending on authentication/identity validity. Notes:
  • This endpoint uses SyncIdentityHandler under the hood to find/create the internal User based on an external identity with sub telegram:{telegramUserId}.
  • Calling this endpoint will create or update a User via the project identity patterns. Repeated calls for the same telegramUserId must return the same internal user (no duplicates).
  • The endpoint is guarded by JwtAuthGuard + CurrentUserGuard; the caller must present a valid JWT. See "Authentication Gap".

  1. POST /api/bot/debt/groups Authentication: Bearer JWT required Purpose: Create a new Debt Group. The authenticated user becomes the owner. Request DTO: CreateGroupDto Example request JSON: { "name": "Keluarga" } Response success example: { "success": true, "data": { "id": "", "publicId": "RL-1A2B3C", "name": "Keluarga" } } Possible errors:
  • 400 Bad Request: { success:false, error:{ code:'BAD_REQUEST', message:'name required' } }
  • 401 Unauthorized / 403 Forbidden depending on JWT/identity Notes:
  • After create, owner is automatically added as a member.
  • publicId is safe to share and used for joining.

  1. GET /api/bot/debt/groups Authentication: Bearer JWT required Purpose: List Debt Groups the current user is a member of. Request: none Response success example: { "success": true, "data": [ { "id": "", "publicId": "RL-1A2B3C", "name": "Keluarga", "ownerId": "" }, ... ] } Possible errors:
  • 401 Unauthorized Notes:
  • Membership is determined by DebtGroupMember entries.

  1. GET /api/bot/debt/groups/:id Authentication: Bearer JWT required Purpose: Get group detail (people list and basic info). User must be a member. Request: none Response success example: { "success": true, "data": { "id": "", "publicId": "RL-1A2B3C", "name": "Keluarga", "people": [ { "id": "", "name": "Rayyan" }, ... ] } } Possible errors:
  • 404 Not Found: { success:false, error:{ code:'NOT_FOUND', message:'Group not found' } }
  • 403 Forbidden: { success:false, error:{ code:'FORBIDDEN', message:'Not a member' } } Notes:
  • Only members can view group detail.

  1. POST /api/bot/debt/groups/:publicId/join Authentication: Bearer JWT required Purpose: Join a group by its publicId (anyone who has publicId can join as member without owner approval). Request: none Example request: POST /api/bot/debt/groups/RL-1A2B3C/join Response success examples:
  • On new membership created: { "success": true, "data": { "id": "" } }
  • If already a member: { "success": true, "data": { "ok": true } } Possible errors:
  • 404 Not Found: { success:false, error:{ code:'NOT_FOUND', message:'Group not found' } } Notes:
  • Unique constraint prevents duplicate membership.

  1. POST /api/bot/debt/groups/:id/leave Authentication: Bearer JWT required Purpose: Current user leaves the group. Request: none Response success example: { "success": true, "data": { "ok": true } } Possible errors:
  • 404 Not Found: Group not found
  • 401/403: unauthorized Notes:
  • Implementation deletes DebtGroupMember entries for that (groupId, userId).

  1. POST /api/bot/debt/groups/:id/people Authentication: Bearer JWT required (Owner only) Purpose: Owner adds a Person to the Group. Request DTO: CreatePersonDto Example request JSON: { "name": "Rayyan" } Response success example: { "success": true, "data": { "id": "", "name": "Rayyan" } } Possible errors:
  • 404 Not Found: Group not found
  • 403 Forbidden: { success:false, error:{ code:'FORBIDDEN', message:'Only owner can add person' } }
  • 400 Bad Request: { success:false, error:{ code:'BAD_REQUEST', message:'Person with same name already exists in group' } } Notes:
  • Person names are unique per group (DB @@unique([groupId,name])).
  • Person != Telegram User (see Person section).

  1. GET /api/bot/debt/groups/:id/people Authentication: Bearer JWT required Purpose: List People inside a group (member-only). Request: none Response success example: { "success": true, "data": [ { "id": "", "name": "Rayyan" }, ... ] } Possible errors:
  • 403 Forbidden: Not a member Notes:
  • Only active (isDeleted=false) persons are returned.

  1. DELETE /api/bot/debt/groups/:id/people/:personId Authentication: Bearer JWT required (Owner only) Purpose: Soft-delete a Person from the Group. Request: none Response success example: { "success": true, "data": null } Possible errors:
  • 404 Not Found: Group not found
  • 403 Forbidden: Only owner can remove person Notes:
  • Implementation sets isDeleted=true (soft delete). Historical transactions remain and are not removed.
  • After soft-delete this Person cannot be used for new transactions.

  1. POST /api/bot/debt/groups/:id/transactions/debt Authentication: Bearer JWT required (Member) Purpose: Create a DEBT transaction (ledger entry). Request DTO: CreateTransactionDto (example): { "from": "Rayyan", "to": "Krisda", "price": "17.000", "description": "Makan siang", "date": "2026/09/01", // optional, format yyyy/mm/dd "requestId": "telegram-update-12345" } Response success example (created DEBT transaction model as returned by Prisma, wrapped): { "success": true, "data": { "id": "", "groupId": "", "fromPersonId": "", "toPersonId": "", "amount": 17000, "type": "DEBT", "description": "Makan siang", "transactionDate": "2026-09-01T00:00:00.000Z", "createdAt": "2026-09-01T10:00:00.000Z", "createdByUserId": "", "requestId": "telegram-update-12345" } } Possible errors:
  • 400 Bad Request:
    • "from and to required"
    • "from and to cannot be same"
    • "Person not found in group"
    • "requestId is required for idempotency"
    • "Nominal tidak valid. Gunakan format seperti 17000 atau 17.000."
    • "Nominal harus lebih besar dari 0"
    • "Tanggal tidak valid. Gunakan format yyyy/mm/dd"
  • 403 Forbidden: Not a member
  • 404 Not Found: Group not found Notes:
  • requestId is required by service to ensure idempotency; DB also has unique(groupId, requestId) as last-resort protection.
  • price is a string in request; service parses dot separators and stores BigInt.
  • transactionDate stored as date-only (Asia/Jakarta by default if omitted).

  1. POST /api/bot/debt/groups/:id/transactions/payment Authentication: Bearer JWT required (Member) Purpose: Create a PAYMENT ledger entry. If payment exceeds existing debt, a reverse DEBT transaction is created automatically for the remainder. Request DTO: CreateTransactionDto (same shape as DEBT) Example request JSON: { "from": "Rayyan", "to": "Krisda", "price": "60.000", "description": "Bayar lebih", "requestId": "telegram-update-67890" } Response success examples:
  • If payment does not produce remainder (no overpayment): { "success": true, "data": { /* PAYMENT transaction object like DEBT example */ } }
  • If payment > existing debt and remainder produced: { "success": true, "data": { "payment": { /* payment transaction object / }, "remainder": { / created DEBT transaction reversed, i.e. to->from */ } } } Possible errors: same as DEBT endpoint (validation, membership, group not found) Notes:
  • Behavior: create PAYMENT ledger entry; compute existing net between the two persons (aggregating ledger), then if payment > existing positive debt, create a new DEBT transaction in opposite direction for remainder with description: "Sisa pembayaran hutang dari {creatorName}".
  • The ledger is immutable: PAYMENT and resultant DEBT(reverse) are separate records.
  • Avoid double-counting: the net is computed from ledger BEFORE the new PAYMENT; after PAYMENT + possible reverse DEBT, aggregate net will reflect correct result.

  1. GET /api/bot/debt/groups/:id/transactions Authentication: Bearer JWT required (Member) Purpose: Retrieve raw ledger history for group. Query params: page (optional), limit (optional) — current implementation ignores advanced paging; controller accepts page & limit but service returns all transactions for group. Response success example: { "success": true, "data": [ { "id": "", "from": "Rayyan", "to": "Krisda", "amount": "17000", "type": "DEBT", "description": "Makan", "transactionDate": "2026-09-01", "createdAt": "2026-09-01T10:00:00Z" }, ... ] } Possible errors:
  • 403 Forbidden: Not a member Notes:
  • Results are ordered by transactionDate ASC, then createdAt ASC to ensure deterministic ordering.
  • This endpoint is RAW ledger history (detail), not summary.

  1. GET /api/bot/debt/groups/:id/summary Authentication: Bearer JWT required (Member) Purpose: Compute CURRENT NET DEBT for the group using global netting (read-only). The algorithm aggregates ledger and performs deterministic greedy settlement. It does not change the ledger. Response success example: { "success": true, "data": [ { "from": "Rayyan", "to": "Krisda", "amount": "22000" }, ... ] } If no debts remain: { "success": true, "data": [] } Possible errors:
  • 403 Forbidden: Not a member Notes:
  • Summary output is a settlement list: who should pay whom and how much, computed by the server's netting algorithm.
  • Different settlement variants may be valid; the server returns one deterministic settlement (greedy algorithm).

  1. GET /api/bot/debt/groups/:id/detail Authentication: Bearer JWT required (Member) Purpose: Same as GET /transactions — return RAW LEDGER HISTORY ordered by transactionDate ASC, createdAt ASC. Response: same as /transactions endpoint. Notes: For Telegram commands, /TampilkanDetailHutang should call this endpoint.

ERROR MAPPING RayLab Core uses a global exception filter which returns errors as: { "success": false, "error": { "code": "<exception.code || 'INTERNAL_ERROR'>", "message": "<exception.message>" } }

Common error cases (HTTP status and example response):

  • 400 Bad Request
    • Example: invalid amount
    • {"success": false, "error": {"code":"BAD_REQUEST","message":"Nominal tidak valid. Gunakan format seperti 17000 atau 17.000."}}
  • 403 Forbidden
    • Example: not a member or not owner
    • {"success": false, "error": {"code":"FORBIDDEN","message":"Not a member"}}
  • 404 Not Found
    • Example: Group not found
    • {"success": false, "error": {"code":"NOT_FOUND","message":"Group not found"}}
  • 500 Internal Error: unexpected exceptions

Exact messages returned are the exception.message strings thrown by the service (see service code for exact strings).


TELEGRAM BOT INTEGRATION (GUIDE) Runtime flow (recommended): Telegram User -> Telegram Bot -> RayLab Core (/api/bot/debt) with JWT auth

Key points:

  • Each API call requires JWT authentication (JwtAuthGuard). The CurrentUserGuard maps the JWT-validated identity to internal User.
  • The codebase does not provide an automatic JWT issuance endpoint for Telegram Bot on behalf of a Telegram user. Auth flow is based on existing RayLab authentication (Authentik / OIDC / internal JWT generation).

Recommended integration options for Telegram Bot developers (choose one depending on security & user experience):

  1. Per-user interactive login (most secure):

    • The Telegram Bot prompts the user to authenticate via the RayLab web login (OIDC) and obtain a RayLab internal access token. The bot instructs user to paste a short-lived token or do a one-time link flow.
    • Bot uses that token to call /api/bot/debt with Authorization: Bearer .
    • Pros: actions executed under user identity; createdByUserId matches the real user.
    • Cons: requires user interaction and web flow.
  2. Service-account with explicit recorded actor (less ideal):

    • Bot uses a service account JWT to call API. createdByUserId will be service-account id.
    • To preserve traceability, Bot includes requestId and includes in request.body metadata about the Telegram user (but server currently does not accept/verify a claimed telegramUserId). This is less secure because createdByUserId won't match Telegram user.
  3. Implement server-side mapping endpoint (recommended by platform team):

    • Add a secure server endpoint that accepts a Telegram update signed or validated by bot token and exchanges it for a user-specific JWT using SyncIdentityHandler and internal authentication service.
    • This requires changes outside the current feature and must follow security review.

Authentication Gap (explicit):

  • The repository currently expects callers to present JWTs. There is no built-in endpoint to exchange a Telegram user_id for a JWT without user authentication.
  • As a result, Telegram Bot developers must either obtain per-user JWTs via the normal login flow, or use a service account JWT (with tradeoffs), or request project owners to implement a secure bot-to-server auth bridge.

When to call /users/sync?

  • /users/sync maps a Telegram external identity into internal User via SyncIdentityHandler. It should be called when you need the internal user to exist or to refresh data. If you follow per-user login (option 1), explicit /users/sync may not be needed. If you use a server-side bridging approach, call /users/sync as part of the bridging sequence to ensure the internal user exists.
  • Do not call /users/sync unauthenticated. The endpoint is guarded and requires a JWT.

TELEGRAM COMMAND → API MAPPING (examples)

  1. Create Group Telegram: /BuatGroup Keluarga Bot: POST /api/bot/debt/groups Request body: { "name": "Keluarga" } Response: returns group publicId (e.g. RL-1A2B3C) -> Bot shares this with group members to join.

  2. Join Group Telegram: /MasukGroup RL-1A2B3C Bot: POST /api/bot/debt/groups/RL-1A2B3C/join

  3. Add Person Telegram: /TambahOrang Rayyan Bot: POST /api/bot/debt/groups/:id/people Body: { "name": "Rayyan" } Note: only owner can add person (owner determined by JWT caller).

  4. Add Debt Telegram: /TambahHutang Rayyan - Krisda - 17.000 - Makan siang Bot: POST /api/bot/debt/groups/:id/transactions/debt Body: { "from": "Rayyan", "to": "Krisda", "price": "17.000", "description": "Makan siang", "requestId": "telegram-update-12345" } If date provided (yyyy/mm/dd) include "date" field.

  5. Payment Telegram: /BayarHutang Rayyan Krisda 10.000 Bot: POST /api/bot/debt/groups/:id/transactions/payment Body: { "from": "Rayyan", "to": "Krisda", "price": "10.000", "requestId": "telegram-update-67890" }

  6. Detail Telegram: /TampilkanDetailHutang Bot: GET /api/bot/debt/groups/:id/detail Bot shows raw ledger lines as returned.

  7. Summary Telegram: /TampilkanKesimpulanHutang Bot: GET /api/bot/debt/groups/:id/summary Bot shows summarized settlement lines.


EXAMPLE END-TO-END FLOW (minimal)

  1. Owner Alice authenticates via RayLab OIDC, obtains JWT (out of scope for bot automation). Bot now holds Alice's JWT for use.
  2. Alice: /BuatGroup Keluarga Bot: POST /api/bot/debt/groups with Alice's JWT -> receives { publicId: "RL-1A2B3C" } Bot replies: "Group created. Public ID RL-1A2B3C"
  3. Bob obtains publicId and calls /MasukGroup RL-1A2B3C (after Bob has a JWT) Bot: POST /api/bot/debt/groups/RL-1A2B3C/join with Bob's JWT
  4. Alice (owner) adds people: /TambahOrang Rayyan -> POST /api/bot/debt/groups/:id/people { name: 'Rayyan' } /TambahOrang Krisda -> POST /... { name: 'Krisda' }
  5. Bob (member) adds debt: /TambahHutang Rayyan - Krisda - 17000 - Makan siang -> POST transactions/debt with his JWT and requestId based on message id
  6. Someone asks /TampilkanKesimpulanHutang -> GET summary -> Bot displays settlement
  7. Someone asks /TampilkanDetailHutang -> GET detail -> Bot displays ledger lines

IDEMPOTENCY

  • requestId in CreateTransactionDto is required by service. The DB has a unique constraint @@unique([groupId, requestId]).
  • Behavior:
    • If a request with the same (groupId, requestId) already exists, service returns the existing transaction and does not create a duplicate.
    • In concurrent scenarios, DB unique constraint is the final line of defense; clients should retry on unique-violation errors by re-fetching the existing transaction or returning success.

Recommendation for bot developers:

  • Use a stable requestId per Telegram update (e.g., telegram:update:{update_id} or telegram:message:{chat_id}:{message_id}) to allow safe retries.

SECURITY REVIEW (quick)

  1. JWT validation
    • JwtAuthGuard verifies external JWKS (Authentik) or internal secret. Use Authorization: Bearer .
  2. Identity spoofing
    • /users/sync is guarded by JwtAuthGuard; unauthorized clients cannot arbitrarily create arbitrary internal users.
    • However, there is no remote-check confirming that the JWT presented belongs to the telegramUserId included in /users/sync body. The server uses SyncIdentityHandler to map identity from verified claims when CurrentUserGuard is used; but the sync endpoint currently expects authenticated callers. If a bot uses a service account JWT, it can call /users/sync for any telegramUserId — the system will create internal users based on given sub telegram:{id}. This is a gap: a bot acting on behalf of a Telegram user must obtain a JWT THAT represents that user, or an auth bridge must be implemented.
  3. Group membership and owner
    • Owner checks compare req.currentUser.id to group.ownerId (server-side). This is enforced server-side.
  4. Person name manipulation
    • Person lookup in createTransaction is by name within groupId and isDeleted=false. Be careful: names are the identity used in the API; bots must ensure proper escaping and normalization when parsing from Telegram.
  5. requestId manipulation
    • requestId is honored by server and unique constraint used; ensure bots generate collision-resistant ids.
  6. DTO whitelist
    • Global ValidationPipe is enabled (whitelist:true, transform:true, forbidNonWhitelisted:true) so unexpected fields are rejected.
  7. SQL/Prisma injection
    • Prisma queries use parameterized API. However, person lookup is by name; ensure the bot sends sanitized strings. Prisma prevents SQL injection when using its query methods.
  8. Deleted Person
    • Soft-deleted Person cannot be used for new transactions; queries use isDeleted=false. Historical transactions remain readable.

Security finding / gap (explicit):

  • There is no built-in server-side flow that allows the Telegram Bot to obtain a JWT on behalf of a Telegram user without user interaction. This means a bot cannot impersonate the real user securely unless that user performs the standard authentication flow (OIDC). If you plan to let Bot perform actions as real users without user login, you must implement a secure delegation mechanism (requires architectural review).

SWAGGER

  • RayLab Core includes Swagger support (if SWAGGER_ENABLED=true in config, docs available). The debt controller classes use DTOs; if Swagger is enabled the endpoints will appear. The project already includes @nestjs/swagger — enabling will include these endpoints.

TESTS / BUILD / MIGRATION STATUS

  • Unit tests (run locally during development): all unit tests in this repo passed in this environment.
    • Test suites run: 10
    • Tests: 29 passed
  • Build: tsc compile succeeded in this environment.
  • Prisma generate: succeeded (Prisma Client generated locally).
  • Prisma migrate dev --name add_debt_models: NOT APPLIED in this environment due to unreachable DB (P1001: Can't reach database server at postgres-raylab:5432).
    • Action required: run migration in environment with reachable DB; do not run migrate reset in production.

FILES CHANGED (by implementation)

  • prisma/schema.prisma (added debt models + backrefs)
  • src/modules/bot-debt/debt.module.ts
  • src/modules/bot-debt/presentation/debt.controller.ts
  • src/modules/bot-debt/presentation/dto/sync-user.dto.ts
  • src/modules/bot-debt/presentation/dto/create-group.dto.ts
  • src/modules/bot-debt/presentation/dto/create-person.dto.ts
  • src/modules/bot-debt/presentation/dto/create-transaction.dto.ts
  • src/modules/bot-debt/application/debt.service.ts
  • src/app.module.ts
  • tests/unit/debt.service.spec.ts

REMAINING ISSUES / ACTIONS FOR OPERATIONAL TEAM

  1. Apply Prisma migration in environment where database is reachable:
    • npx prisma generate
    • npx prisma migrate dev --name add_debt_models
    • Commit the generated migration directory if your process requires it.
  2. Decide on authentication flow for Telegram Bot:
    • Option A (recommended): Bot guides users to obtain per-user JWT via OIDC login; Bot uses users' JWT to call API.
    • Option B: Implement a secure server-side exchange for Telegram identities to internal JWTs (requires new endpoint & security review).
  3. Optionally make requestId DB column NOT NULL to enforce client discipline. (Current code requires requestId in service; DB column is nullable — left intentionally to maintain compatibility.)
  4. Add integration tests (optional): minimal end-to-end test that runs migrate + spins up app and exercises endpoints.

SUMMARY STATEMENT This document is the canonical API contract for the Debt Management endpoints implemented at /api/bot/debt/. It contains concrete request & response JSON, error mapping, identity & integration guidance for Telegram Bot implementers, and notes about security and idempotency.

Next steps for Bot developers:

  • Decide how your bot will obtain JWTs (per-user or service-account). If per-user, implement a login flow; if service-account, accept createdByUserId will be the service account.
  • Implement stable requestId generation per Telegram update to ensure idempotency.
  • Use DTO field names exactly as documented above.

If you want, I can now:

  • Add explicit Swagger decorators/examples to the controllers (so generated API docs include exact DTOs and examples), and
  • Add a small integration test (if you can provide reachable test DB credentials or run the migration locally and provide migration status).