bot telegram debt init
Deploy / deploy (push) Successful in 1m4s

This commit is contained in:
Rayyan
2026-09-03 00:35:24 +07:00
parent e6c115164e
commit c3ce5c2f22
11 changed files with 1248 additions and 2 deletions
+554
View File
@@ -0,0 +1,554 @@
# 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 <token> (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": "<internal-user-uuid>",
"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".
---
2) 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": "<group-uuid>", "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.
---
3) 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": "<group-uuid>", "publicId": "RL-1A2B3C", "name": "Keluarga", "ownerId": "<owner-uuid>" },
...
]
}
Possible errors:
- 401 Unauthorized
Notes:
- Membership is determined by DebtGroupMember entries.
---
4) 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": "<group-uuid>",
"publicId": "RL-1A2B3C",
"name": "Keluarga",
"people": [ { "id": "<person-uuid>", "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.
---
5) 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": "<membership-uuid>" }
}
- 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.
---
6) 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).
---
7) 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": "<person-uuid>", "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).
---
8) 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": "<person-uuid>", "name": "Rayyan" }, ... ]
}
Possible errors:
- 403 Forbidden: Not a member
Notes:
- Only active (isDeleted=false) persons are returned.
---
9) 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.
---
10) 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": "<tx-uuid>",
"groupId": "<group-uuid>",
"fromPersonId": "<person-uuid>",
"toPersonId": "<person-uuid>",
"amount": 17000,
"type": "DEBT",
"description": "Makan siang",
"transactionDate": "2026-09-01T00:00:00.000Z",
"createdAt": "2026-09-01T10:00:00.000Z",
"createdByUserId": "<user-uuid>",
"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).
---
11) 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.
---
12) 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": "<tx-uuid>", "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.
---
13) 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).
---
14) 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 <token>.
- 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 <token>.
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).