bottleneckcrm/mobile/lib/data/models/audit_entry.dart
Ashwin ed4263a534 Refactor tests and add new functionality for ticket closing and mailbox handling
- Updated phone viewport test to check for new text related to SNS topics.
- Enhanced audit log screen tests with new credential events and filters.
- Modified mailbox form sheet tests to handle SNS Topic ARN correctly.
- Introduced new tests for ticket form close date functionality, ensuring correct behavior for optional close dates.
- Added support for organization timezone handling in tests, ensuring accurate date calculations based on user organization.
- Improved unit tests for mailbox and organization date handling, ensuring proper behavior for various scenarios.
- Implemented bulk close date functionality in ticket selection tests, verifying correct behavior for date selection and submission.
2026-09-27 11:43:51 +05:30

133 lines
4.7 KiB
Dart

/// Entries from `GET /api/org/audit-log/`, mirroring `/settings/audit-log`
/// on the web.
///
/// The API sends only allow-listed `details` keys (ids, counts, and sentences
/// the server wrote) and never the stored description, so everything shown is
/// built from those.
library;
/// One option in the event filter.
class AuditEventType {
const AuditEventType({required this.value, required this.label});
final String value;
final String label;
factory AuditEventType.fromJson(Map<String, dynamic> json) => AuditEventType(
value: json['value']?.toString() ?? '',
label: json['label']?.toString() ?? '',
);
}
class AuditEntry {
const AuditEntry({
required this.id,
required this.eventType,
required this.eventLabel,
this.createdAt,
this.success = true,
this.actorId,
this.actorName = '',
this.actorEmail = '',
this.ipAddress = '',
this.details = const {},
});
final String id;
final String eventType;
final String eventLabel;
final DateTime? createdAt;
final bool success;
/// Null for a system event, or once the user has been deleted.
final String? actorId;
final String actorName;
final String actorEmail;
final String ipAddress;
final Map<String, dynamic> details;
String get actorLabel {
if (actorId == null) return 'No user';
if (actorName.isNotEmpty) return actorName;
if (actorEmail.isNotEmpty) return actorEmail;
return 'No user';
}
/// One line on what happened, or '' when the label says it all. The same
/// rules as `auditDetail` in `frontend/src/lib/v2/audit-log.js`.
String get detail {
final reason = details['pause_reason'];
if (reason != null && reason.toString().isNotEmpty) return '$reason';
if (eventType == 'RECORD_MERGED' && details['merged_name'] != null) {
final entity = details['entity']?.toString() ?? '';
// Duplicates usually share a name, and "X into X" says nothing, so
// equal names each carry the start of their id.
final same = details['merged_name'] == details['kept_name'];
String tag(Object? id) {
final text = id?.toString() ?? '';
if (!same || text.isEmpty) return '';
return ' (${text.length > 8 ? text.substring(0, 8) : text})';
}
return 'Merged ${entity.isEmpty ? 'record' : entity} '
'"${details['merged_name']}"${tag(details['merged_id'])} '
'into "${details['kept_name']}"${tag(details['kept_id'])}.';
}
final prefix = details['token_prefix'];
if ((eventType == 'API_TOKEN_CREATED' ||
eventType == 'API_TOKEN_REVOKED') &&
prefix != null &&
prefix.toString().isNotEmpty) {
// An empty scope list is a token with the owner's full access. The
// owner is named only when someone else acted: an admin revoking it.
final scopes = details['scopes'];
final scopeText = scopes is List && scopes.isNotEmpty
? 'scopes ${scopes.join(', ')}'
: 'full access';
final ownerId = details['owner_id'];
final owner = ownerId != null && ownerId.toString() != actorId
? ', owned by ${details['owner_name']}'
: '';
return 'Token "${details['token_name']}" ($prefix), $scopeText$owner.';
}
if (eventType == 'WEBHOOK_REENABLED') {
return 'Turned back on, and now answers for the webhook.';
}
final changed = details['changed'];
if (eventType == 'WEBHOOK_CHANGED' && changed is List) {
return 'Changed ${changed.join(', ')}, and now answers for the webhook.';
}
final action = details['action'];
final resource = details['resource'];
if (action != null && resource != null) return '$action on $resource';
final deleted = details['deleted_count'];
if (deleted != null) return '$deleted sample leads removed';
return '';
}
/// The webhook the entry is about, for a link, or null.
String? get webhookId {
final id = details['endpoint_id'];
return id is String && id.isNotEmpty ? id : null;
}
factory AuditEntry.fromJson(Map<String, dynamic> json) {
final actor = json['actor'];
final details = json['details'];
return AuditEntry(
id: json['id']?.toString() ?? '',
eventType: json['event_type']?.toString() ?? '',
eventLabel:
json['event_label']?.toString() ??
json['event_type']?.toString() ??
'',
createdAt: DateTime.tryParse(json['created_at']?.toString() ?? ''),
success: json['success'] != false,
actorId: actor is Map ? actor['id']?.toString() : null,
actorName: actor is Map ? actor['name']?.toString() ?? '' : '',
actorEmail: actor is Map ? actor['email']?.toString() ?? '' : '',
ipAddress: json['ip_address']?.toString() ?? '',
details: details is Map<String, dynamic> ? details : const {},
);
}
}