Files
tm_app/lib/data/repositories/company_ai_repository.dart
T
AmirReza Jamali 50e4f43738 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>
2026-06-16 11:47:52 +03:30

89 lines
3.4 KiB
Dart

import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/constants/pref_keys.dart';
import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/company_ai_service.dart';
import 'package:tm_app/data/services/model/onboarding_response/onboarding_response.dart';
import 'package:tm_app/data/services/model/recommend_response/recommend_response.dart';
/// Outcome of [CompanyAiRepository.maybeStartOnboarding].
enum AiOnboardingOutcome {
/// Onboarding was started against the AI service.
started,
/// Skipped: the company has no documents and no website yet.
skippedNoData,
/// Skipped: this profile version was already onboarded.
skippedUnchanged,
/// The onboarding request failed (network/server). Safe to retry later.
failed,
}
/// Coordinates AI onboarding and recommendation calls.
///
/// Onboarding is asynchronous on the AI side and only needs to run when the
/// company's documents or website change, so this repository fingerprints that
/// data in [SharedPreferences] and skips redundant calls — mirroring the
/// `onCompanyProfileReady` guidance in the mobile integration guide.
class CompanyAiRepository {
CompanyAiRepository({
required CompanyAiService companyAiService,
required SharedPreferences prefs,
}) : _companyAiService = companyAiService,
_prefs = prefs;
final CompanyAiService _companyAiService;
final SharedPreferences _prefs;
/// Starts onboarding only when the company has data to index and its
/// fingerprint differs from the last successful run.
Future<AiOnboardingOutcome> maybeStartOnboarding({
required List<String> documentFileIds,
required String? website,
}) async {
final hasWebsite = website != null && website.trim().isNotEmpty;
if (documentFileIds.isEmpty && !hasWebsite) {
return AiOnboardingOutcome.skippedNoData;
}
final fingerprint = _fingerprint(documentFileIds, website);
if (fingerprint == _prefs.getString(PrefKeys.onboardingFingerprint)) {
return AiOnboardingOutcome.skippedUnchanged;
}
final result = await _companyAiService.startOnboarding();
switch (result) {
case Ok<OnboardingResponse>():
await _prefs.setString(PrefKeys.onboardingFingerprint, fingerprint);
await _prefs.setInt(
PrefKeys.onboardingStartedAt,
DateTime.now().millisecondsSinceEpoch,
);
AppLogger().info('✅ AI onboarding started for fingerprint $fingerprint');
return AiOnboardingOutcome.started;
case Error<OnboardingResponse>():
// Don't persist the fingerprint so the next profile load retries.
AppLogger().logWarning('⚠️ AI onboarding failed: ${result.error}');
return AiOnboardingOutcome.failed;
}
}
Future<Result<RecommendResponse>> getRecommendations() {
return _companyAiService.getRecommendations();
}
/// Epoch milliseconds of the last started onboarding, or null if none.
int? lastOnboardingStartedAt() =>
_prefs.getInt(PrefKeys.onboardingStartedAt);
/// Stable, order-independent fingerprint of the data the AI service indexes.
/// Stored verbatim (it is not sensitive) so it compares reliably across app
/// launches, unlike [String.hashCode].
String _fingerprint(List<String> documentFileIds, String? website) {
final ids = [...documentFileIds]..sort();
return '${ids.join(',')}|${website?.trim() ?? ''}';
}
}