You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Infrastructure & Database: Deployed on OCI (Oracle Cloud Infrastructure) using Terraform. The database is an Oracle Autonomous Database provisioned in OCI.
All PKs use NUMBER GENERATED ALWAYS AS IDENTITY. All tables have created_at DEFAULT CURRENT_TIMESTAMP. No updated_at — history is covered by AUDIT_LOG.
APP_USER
Column
Type
Notes
id
NUMBER PK
full_name
VARCHAR2
email
VARCHAR2 UNIQUE
password_hash
VARCHAR2
BCrypt
telegram_chat_id
NUMBER UNIQUE
nullable; linked via /login bot command
is_active
NUMBER(1)
soft-delete flag
created_at
TIMESTAMP
PROJECT
Column
Type
Notes
id
NUMBER PK
project_name
VARCHAR2
description
CLOB
nullable
status
VARCHAR2
CHECK: ACTIVE / PAUSED / CLOSED
manager
NUMBER FK → APP_USER
NOT NULL; creator becomes manager
created_at
TIMESTAMP
PROJECT_MEMBER
Column
Type
Notes
id
NUMBER PK
project
NUMBER FK → PROJECT
employee
NUMBER FK → APP_USER
created_at
TIMESTAMP
UNIQUE(project, employee). Manager is not duplicated here.
Only one ACTIVE sprint per project at a time. activateSprint() throws ConflictException if one already exists.
Deleting a sprint moves all its tasks to backlog (sets sprint = NULL). Cannot delete an ACTIVE sprint.
CLOSED sprint tasks are read-only — any mutation throws ClosedSprintException.
Task Rules
sprint = NULL means the task is in the backlog of that project.
story_points is required (integer, > 0).
priority is optional (LOW / MEDIUM / HIGH).
assigned_to is nullable.
Transitioning to DONErequiresactual_hours > 0 in the request body.
actual_hours is overwritten on every DONE transition. Reopening a task leaves it unchanged.
Task's sprint must belong to the same project as the task (validated in service layer).
Project Lifecycle
Creating a project sets status ACTIVE and auto-creates a PROJECT_MEMBER row for the manager.
closeProject() auto-closes all PLANNING and ACTIVE sprints, then sets project to CLOSED.
Response body includes count of affected sprints and pending tasks.
transferProject() requires the new manager to already be a PROJECT_MEMBER.
Physical deletion: deleteProject() cascades deletes of activities → tasks → sprints → members → project.
Permission Checks
// Manager guardpublicvoidrequireManager(LonguserId, Projectproject) {
if (!project.getManager().getId().equals(userId))
thrownewForbiddenException("Only the project manager can perform this action");
}
// Task status change — manager OR assigneebooleanisManager = project.getManager().getId().equals(userId);
booleanisAssigned = task.getAssignedTo() != null && task.getAssignedTo().getId().equals(userId);
if (!isManager && !isAssigned)
thrownewForbiddenException("Only the project manager or assigned user can change task status");
changeStatus: manager OR assigned user. On DONE: validates actualHours > 0, sets task.actualHours. Appends STATUS_CHANGE activity. Fires TASK_BLOCKED notification to manager, and TASK_STATUS_CHANGE to assignee (both best-effort, ignored on failure).
oldValue and newValue are serialized to JSON (Jackson ObjectMapper) and stored as CLOB.
The audit log write is part of the same @Transactional as the business operation — if it fails, the business operation rolls back.
TaskActivity (append-only log)
Three activity types are created automatically:
STATUS_CHANGE — on every changeStatus() call, content = "TODO → IN_PROGRESS".
SPRINT_CHANGE — on changeSprint(), content = "Moved from sprint X to Y" (backlog if null).
COMMENT — via TaskActivityService.addComment(), content = free text.
10. Notifications
NotificationService
send(recipient, eventType, message): delivers via Telegram if telegram_chat_id is set on the recipient, logs to NOTIFICATION_LOG.
retryFailed(): re-sends entries where delivery_status = FAILED.
Notification failures never roll back the business operation (caught and ignored in TaskService).
NotificationScheduler
Two @Scheduled(cron = "0 0 9 * * *") jobs run daily at 09:00:
Sprint deadline warning: finds all ACTIVE sprints ending within 3 days, skips if all tasks are DONE, sends message to manager + each assigned member with incomplete tasks.
Bot only starts if TELEGRAM_BOT_TOKEN environment variable is set.
Message routing
message received
├── starts with '/' → routeCommand(command, update)
└── plain text → handleNaturalLanguage(text, update, chatId)
Command handlers
Command
Handler class
Description
/start
StartHandler
Welcome message
/login
LoginHandler
Links Telegram chat_id to APP_USER account
/help
HelpHandler
Lists all available commands
/my_projects
MyProjectsHandler
Lists user's projects
/my_tasks
MyTasksHandler
Lists tasks assigned to the user
/task {id}
TaskHandler
Shows task detail
/task_status {id} {status} [hours]
TaskStatusHandler
Changes task status (hours required for DONE)
/comment {id} {text}
CommentHandler
Adds a comment to a task
Natural Language fallback
Non-command text is sent to NaturalLanguageRouter. If NLU returns a valid command, SyntheticUpdateFactory wraps the extracted params into a fake Update object and routeCommand is called transparently.
12. NLU Router (AI)
NaturalLanguageRouter calls Anthropic Claude (claude-sonnet-4-6) to classify free-text messages into bot commands.
Activation
Requires ANTHROPIC_API_KEY environment variable. If absent, every NLU call returns NluResult.unknown() and the bot replies with the help hint.
API call
Endpoint: https://api.anthropic.com/v1/messages
Max tokens: 150
System prompt: instructs Claude to respond with JSON only, map to one of 7 commands, and extract integer IDs.
After a successful NLU classification, KairoBot.validateNluParams() checks that any id param is a pure integer. If not, a user-friendly error is sent via NluErrorMessages.
13. Dashboard & KPI Endpoints
All dashboard endpoints are under /api/v1/projects/{projectId}/dashboard/ and require project participation. Sprint parameter is optional — defaults to the active sprint.