feat: add AI recommendations flow for tenders

Implement company AI onboarding/recommendation models, services, repository, and tender UI integration with supporting tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
AmirReza Jamali
2026-06-16 11:47:52 +03:30
parent 83b32549ce
commit 50e4f43738
34 changed files with 2558 additions and 112 deletions
@@ -14,6 +14,11 @@ abstract class CompanyProfileData with _$CompanyProfileData {
required String? industry,
@JsonKey(name: 'founded_year') required int? foundedYear,
@JsonKey(name: 'created_at') required int? createdAt,
// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
@JsonKey(name: 'document_file_ids') List<String>? documentFileIds,
String? website,
}) = _CompanyProfileData;
factory CompanyProfileData.fromJson(Map<String, dynamic> json) =>
@@ -15,7 +15,10 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CompanyProfileData {
String? get id; String? get name;@JsonKey(name: 'registration_number') String? get registrationNumber; String? get industry;@JsonKey(name: 'founded_year') int? get foundedYear;@JsonKey(name: 'created_at') int? get createdAt;
String? get id; String? get name;@JsonKey(name: 'registration_number') String? get registrationNumber; String? get industry;@JsonKey(name: 'founded_year') int? get foundedYear;@JsonKey(name: 'created_at') int? get createdAt;// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
@JsonKey(name: 'document_file_ids') List<String>? get documentFileIds; String? get website;
/// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +31,16 @@ $CompanyProfileDataCopyWith<CompanyProfileData> get copyWith => _$CompanyProfile
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&const DeepCollectionEquality().equals(other.documentFileIds, documentFileIds)&&(identical(other.website, website) || other.website == website));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt);
int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt,const DeepCollectionEquality().hash(documentFileIds),website);
@override
String toString() {
return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt)';
return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt, documentFileIds: $documentFileIds, website: $website)';
}
@@ -48,7 +51,7 @@ abstract mixin class $CompanyProfileDataCopyWith<$Res> {
factory $CompanyProfileDataCopyWith(CompanyProfileData value, $Res Function(CompanyProfileData) _then) = _$CompanyProfileDataCopyWithImpl;
@useResult
$Res call({
String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt
String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website
});
@@ -65,7 +68,7 @@ class _$CompanyProfileDataCopyWithImpl<$Res>
/// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,Object? documentFileIds = freezed,Object? website = freezed,}) {
return _then(_self.copyWith(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -73,7 +76,9 @@ as String?,registrationNumber: freezed == registrationNumber ? _self.registratio
as String?,industry: freezed == industry ? _self.industry : industry // ignore: cast_nullable_to_non_nullable
as String?,foundedYear: freezed == foundedYear ? _self.foundedYear : foundedYear // ignore: cast_nullable_to_non_nullable
as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int?,
as int?,documentFileIds: freezed == documentFileIds ? _self.documentFileIds : documentFileIds // ignore: cast_nullable_to_non_nullable
as List<String>?,website: freezed == website ? _self.website : website // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -158,10 +163,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CompanyProfileData() when $default != null:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt);case _:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt,_that.documentFileIds,_that.website);case _:
return orElse();
}
@@ -179,10 +184,10 @@ return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_tha
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website) $default,) {final _that = this;
switch (_that) {
case _CompanyProfileData():
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt);case _:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt,_that.documentFileIds,_that.website);case _:
throw StateError('Unexpected subclass');
}
@@ -199,10 +204,10 @@ return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_tha
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website)? $default,) {final _that = this;
switch (_that) {
case _CompanyProfileData() when $default != null:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt);case _:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt,_that.documentFileIds,_that.website);case _:
return null;
}
@@ -214,7 +219,7 @@ return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_tha
@JsonSerializable()
class _CompanyProfileData implements CompanyProfileData {
const _CompanyProfileData({required this.id, required this.name, @JsonKey(name: 'registration_number') required this.registrationNumber, required this.industry, @JsonKey(name: 'founded_year') required this.foundedYear, @JsonKey(name: 'created_at') required this.createdAt});
const _CompanyProfileData({required this.id, required this.name, @JsonKey(name: 'registration_number') required this.registrationNumber, required this.industry, @JsonKey(name: 'founded_year') required this.foundedYear, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'document_file_ids') final List<String>? documentFileIds, this.website}): _documentFileIds = documentFileIds;
factory _CompanyProfileData.fromJson(Map<String, dynamic> json) => _$CompanyProfileDataFromJson(json);
@override final String? id;
@@ -223,6 +228,22 @@ class _CompanyProfileData implements CompanyProfileData {
@override final String? industry;
@override@JsonKey(name: 'founded_year') final int? foundedYear;
@override@JsonKey(name: 'created_at') final int? createdAt;
// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
final List<String>? _documentFileIds;
// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
@override@JsonKey(name: 'document_file_ids') List<String>? get documentFileIds {
final value = _documentFileIds;
if (value == null) return null;
if (_documentFileIds is EqualUnmodifiableListView) return _documentFileIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final String? website;
/// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values.
@@ -237,16 +258,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&const DeepCollectionEquality().equals(other._documentFileIds, _documentFileIds)&&(identical(other.website, website) || other.website == website));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt);
int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt,const DeepCollectionEquality().hash(_documentFileIds),website);
@override
String toString() {
return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt)';
return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt, documentFileIds: $documentFileIds, website: $website)';
}
@@ -257,7 +278,7 @@ abstract mixin class _$CompanyProfileDataCopyWith<$Res> implements $CompanyProfi
factory _$CompanyProfileDataCopyWith(_CompanyProfileData value, $Res Function(_CompanyProfileData) _then) = __$CompanyProfileDataCopyWithImpl;
@override @useResult
$Res call({
String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt
String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website
});
@@ -274,7 +295,7 @@ class __$CompanyProfileDataCopyWithImpl<$Res>
/// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,Object? documentFileIds = freezed,Object? website = freezed,}) {
return _then(_CompanyProfileData(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -282,7 +303,9 @@ as String?,registrationNumber: freezed == registrationNumber ? _self.registratio
as String?,industry: freezed == industry ? _self.industry : industry // ignore: cast_nullable_to_non_nullable
as String?,foundedYear: freezed == foundedYear ? _self.foundedYear : foundedYear // ignore: cast_nullable_to_non_nullable
as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int?,
as int?,documentFileIds: freezed == documentFileIds ? _self._documentFileIds : documentFileIds // ignore: cast_nullable_to_non_nullable
as List<String>?,website: freezed == website ? _self.website : website // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -14,6 +14,11 @@ _CompanyProfileData _$CompanyProfileDataFromJson(Map<String, dynamic> json) =>
industry: json['industry'] as String?,
foundedYear: (json['founded_year'] as num?)?.toInt(),
createdAt: (json['created_at'] as num?)?.toInt(),
documentFileIds:
(json['document_file_ids'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
website: json['website'] as String?,
);
Map<String, dynamic> _$CompanyProfileDataToJson(_CompanyProfileData instance) =>
@@ -24,4 +29,6 @@ Map<String, dynamic> _$CompanyProfileDataToJson(_CompanyProfileData instance) =>
'industry': instance.industry,
'founded_year': instance.foundedYear,
'created_at': instance.createdAt,
'document_file_ids': instance.documentFileIds,
'website': instance.website,
};