uto_manager/lib/domain/value_objects/duty_mos.dart

237 lines
5.9 KiB
Dart

import 'package:meta/meta.dart';
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
/// Represents a validated Military Occupational Specialty (MOS) code.
///
/// MOS codes follow the U.S. Army standard format: 2 digits + 1 letter
/// (e.g., "11B", "68W", "13F"). Aviation warrant officer AOCs use a
/// 3-digit + 1-letter format (e.g., "153A"). Both formats are accepted.
/// This value object enforces that format and validates against the known
/// MOS dictionary.
@immutable
class DutyMos {
/// Creates a [DutyMos] from a raw MOS code string.
///
/// Throws [ArgumentError] if the code is not a valid 3-character MOS.
factory DutyMos(String code) {
final normalized = code.trim().toUpperCase();
if (!_isValidFormat(normalized)) {
throw ArgumentError(
'Invalid MOS code format: "$code". '
'Expected format: 2 digits + 1 letter (e.g., "11B", "68W") '
'or 3 digits + 1 letter for aviation AOCs (e.g., "153A").',
);
}
if (!_knownMosCodes.contains(normalized)) {
throw ArgumentError(
'Unknown MOS code: "$normalized". '
'Code is not in the authorized MOS dictionary.',
);
}
return DutyMos._(normalized);
}
/// Creates a [DutyMos] without dictionary validation (for testing/migration).
factory DutyMos.unvalidated(String code) {
final normalized = code.trim().toUpperCase();
if (!_isValidFormat(normalized)) {
throw ArgumentError(
'Invalid MOS code format: "$code". '
'Expected format: 2 digits + 1 letter (e.g., "11B", "68W") '
'or 3 digits + 1 letter for aviation AOCs (e.g., "153A").',
);
}
return DutyMos._(normalized);
}
const DutyMos._(this.code);
/// The normalized MOS code string (e.g., "11B", "68W", "13F").
final String code;
/// Returns true if [code] matches a valid MOS format:
/// - Standard: 2 digits + 1 letter (e.g., "11B", "68W")
/// - Aviation AOC: 3 digits + 1 letter (e.g., "153A")
static bool _isValidFormat(String code) {
// Standard 3-char format: NNx
if (code.length == 3) {
final digits = code.substring(0, 2);
final letter = code[2];
return RegExp(r'^\d{2}$').hasMatch(digits) && RegExp(r'^[A-Z]$').hasMatch(letter);
}
// Aviation AOC 4-char format: NNNx
if (code.length == 4) {
final digits = code.substring(0, 3);
final letter = code[3];
return RegExp(r'^\d{3}$').hasMatch(digits) && RegExp(r'^[A-Z]$').hasMatch(letter);
}
return false;
}
/// Returns true if [code] is a known MOS in the authorized dictionary.
static bool isKnown(String code) {
return _knownMosCodes.contains(code.trim().toUpperCase());
}
/// Returns all known MOS codes as a sorted list.
static List<String> get allCodes => _knownMosCodes.toList()..sort();
/// Returns all MOS codes for a given [DoctrinalBranch].
static Set<String> codesForBranch(DoctrinalBranch branch) {
return _branchMosMap[branch] ?? {};
}
/// The complete set of authorized MOS codes.
static const Set<String> _knownMosCodes = {
// Infantry & Maneuver (CMF 11 / 15)
'11A', '11B', '11C', '15W', '15A', '15T', '15B', '15D', '15G', '153A',
// Medical (CMF 68)
'68W', '68A', '68D', '68E', '68F', '68G', '68J',
'68K', '68P', '68Q', '68R', '68S', '68T', '68X',
// Field Artillery (CMF 13)
'13A', '13B', '13D', '13F', '13J', '13M', '13P', '13R', '13T',
// Signal (CMF 25)
'25A', '25B', '25C', '25D', '25E', '25H', '25L', '25M',
'25N', '25P', '25Q', '25R', '25S', '25U', '25V', '25W', '25X', '25Z',
// Cyber/EW (CMF 17)
'17A', '17C', '17E', '17S',
// Aviation (CMF 15)
'15Z',
// Logistics (CMF 91/92)
'91A', '91B', '91C', '91D', '91E', '91F', '91G', '91H',
'91J', '91L', '91M', '91P', '91S', '91X', '91Z',
'92A', '92F', '92G', '92L', '92M', '92R', '92S', '92W', '92Y', '92Z',
// Engineer (CMF 12)
'12A', '12B', '12C', '12D', '12G', '12H', '12K', '12M',
'12N', '12P', '12Q', '12R', '12T', '12W', '12X', '12Y', '12Z',
};
/// Maps each [DoctrinalBranch] to its associated MOS codes.
static const Map<DoctrinalBranch, Set<String>> _branchMosMap = {
DoctrinalBranch.infantry: {
'11A',
'11B',
'11C',
'15W',
'15A',
'15T',
'153A',
},
DoctrinalBranch.medical: {
'68W',
'68A',
'68D',
'68E',
'68F',
'68G',
'68J',
'68K',
'68P',
'68Q',
'68R',
'68S',
'68T',
'68X',
},
DoctrinalBranch.fires: {
'13A',
'13B',
'13D',
'13F',
'13J',
'13M',
'13P',
'13R',
'13T',
},
DoctrinalBranch.signal: {
'25A',
'25B',
'25C',
'25D',
'25E',
'25H',
'25L',
'25M',
'25N',
'25P',
'25Q',
'25R',
'25S',
'25U',
'25V',
'25W',
'25X',
'25Z',
},
DoctrinalBranch.cyberEw: {'17A', '17C', '17E', '17S'},
DoctrinalBranch.aviation: {
'15A',
'15B',
'15D',
'15G',
'15T',
'15W',
'15Z',
'153A',
},
DoctrinalBranch.logistics: {
'91A',
'91B',
'91C',
'91D',
'91E',
'91F',
'91G',
'91H',
'91J',
'91L',
'91M',
'91P',
'91S',
'91X',
'91Z',
'92A',
'92F',
'92G',
'92L',
'92M',
'92R',
'92S',
'92W',
'92Y',
'92Z',
},
DoctrinalBranch.engineer: {
'12A',
'12B',
'12C',
'12D',
'12G',
'12H',
'12K',
'12M',
'12N',
'12P',
'12Q',
'12R',
'12T',
'12W',
'12X',
'12Y',
'12Z',
},
};
@override
bool operator ==(Object other) =>
identical(this, other) || (other is DutyMos && other.code == code);
@override
int get hashCode => code.hashCode;
@override
String toString() => code;
}