AI Agent Context: Action Reasons
High-density technical context for AI agents working on the Action Reasons feature.
Quick Facts
- Purpose: Admin-managed lookup table for predefined reasons in contract workflows
- Location:
services/partner-service/src/modules/action-reasons/
- Route prefix:
/api/partners/admin/action-reasons
- Database table:
action_reasons
- Status: Active, production-ready
Entry Points
| Type |
Path |
| Controller |
src/modules/action-reasons/action-reasons.controller.ts |
| Service |
src/modules/action-reasons/action-reasons.service.ts |
| Entity |
src/modules/action-reasons/entities/action-reason.entity.ts |
| DTOs |
src/modules/action-reasons/dto/ |
| DB Schema |
src/common/database/database.service.ts (lines 812-865) |
Database Schema
CREATE TABLE IF NOT EXISTS action_reasons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
context VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
display_order INTEGER NOT NULL DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_action_reasons_context_slug UNIQUE (context, slug)
);
-- Indexes
CREATE INDEX idx_action_reasons_context ON action_reasons(context);
CREATE INDEX idx_action_reasons_enabled ON action_reasons(enabled);
-- FK columns on partner_contracts
ALTER TABLE partner_contracts ADD COLUMN void_reason_id UUID;
ALTER TABLE partner_contracts ADD COLUMN changes_requested_reason_id UUID;
-- FKs reference action_reasons(id) ON DELETE SET NULL
API Endpoints
| Method |
Path |
Handler |
Purpose |
| GET |
/ |
list() |
List by context, optionally include disabled |
| GET |
/:id |
getById() |
Single reason lookup |
| POST |
/ |
create() |
Create new reason |
| PATCH |
/:id |
update() |
Partial update |
| DELETE |
/:id |
delete() |
Hard delete |
| PUT |
/reorder |
reorder() |
Bulk update display_order |
Context Constants
export const ACTION_REASON_CONTEXTS = {
CONTRACT_VOID: 'contract_void',
CONTRACT_CHANGES_REQUESTED: 'contract_changes_requested',
} as const;
Service Methods
| Method |
Parameters |
Returns |
Notes |
listByContext(context) |
context: string |
ActionReason[] |
Enabled only, sorted by display_order |
listAllByContext(context) |
context: string |
ActionReason[] |
All reasons, for admin UI |
getById(id) |
id: string |
ActionReason | null |
Single lookup |
create(dto) |
CreateActionReasonDto |
ActionReason |
Auto-increments display_order |
update(id, dto) |
id, UpdateActionReasonDto |
ActionReason |
Throws NotFoundException |
delete(id) |
id: string |
void |
Hard delete, throws NotFoundException |
reorder(context, ids) |
context, string[] |
ActionReason[] |
Updates display_order for all |
DTO Definitions
CreateActionReasonDto
{
context: string; // @IsString @MinLength(1) @MaxLength(100)
slug: string; // @IsString @MinLength(1) @MaxLength(100) @Matches(/^[a-z0-9_-]+$/)
name: string; // @IsString @MinLength(1) @MaxLength(255)
description?: string; // @IsOptional @IsString
display_order?: number; // @IsOptional @IsInt @Min(0)
}
UpdateActionReasonDto
{
name?: string; // @IsOptional @IsString @MinLength(1) @MaxLength(255)
description?: string; // @IsOptional @IsString
display_order?: number; // @IsOptional @IsInt @Min(0)
enabled?: boolean; // @IsOptional @IsBoolean
}
ReorderActionReasonsDto
{
context: string; // @IsString @MinLength(1)
ids: string[]; // @IsArray @IsUUID("4", { each: true })
}
Frontend Files
| Component |
Path |
Purpose |
| Settings Page |
admin-console/src/pages/SettingsPage/ActionReasonsPage.tsx |
CRUD UI with drag-drop |
| Service |
admin-console/src/services/partner.service.ts |
API client methods |
| Contract List |
admin-console/src/components/organisms/PartnerManagement/PartnerContractList.tsx |
Reason selection dropdown |
Frontend Service Methods
// partner.service.ts
listActionReasons(context: string, includeDisabled?: boolean): Promise<ActionReason[]>
getActionReason(id: string): Promise<ActionReason | null>
createActionReason(data: CreateActionReasonDto): Promise<ActionReason>
updateActionReason(id: string, data: UpdateActionReasonDto): Promise<ActionReason>
deleteActionReason(id: string): Promise<void>
reorderActionReasons(context: string, ids: string[]): Promise<ActionReason[]>
Translation Keys
Path: admin-console/src/i18n/locales/en.json
settings.actionReasons.title
settings.actionReasons.subtitle
settings.actionReasons.contractVoid
settings.actionReasons.contractChanges
settings.actionReasons.voidReasons
settings.actionReasons.changesReasons
settings.actionReasons.addReason
settings.actionReasons.editReason
settings.actionReasons.deleteTitle
settings.actionReasons.deleteConfirm
settings.actionReasons.slug
settings.actionReasons.slugHelp
settings.actionReasons.name
settings.actionReasons.description
settings.actionReasons.enabled
settings.actionReasons.noReasons
settings.actionReasons.createFirst
settings.actionReasons.loadError
settings.actionReasons.saveError
settings.actionReasons.createSuccess
settings.actionReasons.updateSuccess
settings.actionReasons.deleteSuccess
settings.actionReasons.deleteError
settings.actionReasons.toggleError
settings.actionReasons.reorderSuccess
settings.actionReasons.reorderError
settings.actionReasons.disabled
settings.actionReasons.errors.slugRequired
settings.actionReasons.errors.slugFormat
settings.actionReasons.errors.slugExists
settings.actionReasons.errors.nameRequired
Common Operations
Add new reason via API
curl -X POST http://localhost:3003/api/partners/admin/action-reasons \
-H "Content-Type: application/json" \
-d '{"context":"contract_void","slug":"test-reason","name":"Test Reason"}'
List reasons for a context
curl "http://localhost:3003/api/partners/admin/action-reasons?context=contract_void"
List all including disabled
curl "http://localhost:3003/api/partners/admin/action-reasons?context=contract_void&includeDisabled=true"
Code Patterns
Creating a reason with auto display_order
// If display_order not provided, service auto-calculates next value
const reason = await actionReasonsService.create({
context: 'contract_void',
slug: 'new-reason',
name: 'New Reason',
});
// display_order will be MAX(display_order) + 1 for context
Checking slug uniqueness
// Done automatically in create() - throws ConflictException if duplicate
// Constraint: uq_action_reasons_context_slug UNIQUE (context, slug)
Integration with contract void
// In partner-contracts.service.ts voidContract method:
// Store void_reason_id alongside existing void_reason text field
await this.databaseService.query(
`UPDATE partner_contracts
SET status = 'VOID', void_reason = $2, void_reason_id = $3
WHERE id = $1`,
[contractId, reasonText, reasonId]
);
Relationships
Dependencies:
→ DatabaseService (PostgreSQL queries)
Dependents:
← PartnerContractsService (reason ID storage)
← Admin Console Settings Page (CRUD UI)
← Admin Console Contract List (reason selection)
Troubleshooting
| Issue |
Cause |
Fix |
| ConflictException on create |
Duplicate slug in context |
Change slug or use different context |
| NotFoundException on update/delete |
Invalid ID |
Verify ID exists in database |
| Empty list on GET |
No enabled reasons |
Add includeDisabled=true or create reasons |
| Reorder fails |
ID not in specified context |
Ensure all IDs belong to the context |
Extension Points
To add a new context:
- Add constant to
ACTION_REASON_CONTEXTS in entity
- Add constant to frontend
partner.service.ts
- Add tab in
ActionReasonsPage.tsx
- Add FK column to relevant table
- Update workflow service to use reason_id