rinogeek commited on
Commit
4e138bb
·
1 Parent(s): c2c51a2
Akompta/settings.py CHANGED
@@ -129,10 +129,10 @@ USE_TZ = True
129
  # Static files (CSS, JavaScript, Images)
130
  # https://docs.djangoproject.com/en/5.2/howto/static-files/
131
 
132
- STATIC_URL = 'static/'
133
  STATIC_ROOT = BASE_DIR / 'static'
134
 
135
- MEDIA_URL = 'media/'
136
  MEDIA_ROOT = BASE_DIR / 'media'
137
 
138
  # Default primary key field type
@@ -198,7 +198,7 @@ SIMPLE_JWT = {
198
  # CORS Configuration
199
  CORS_ALLOWED_ORIGINS = os.environ.get(
200
  'CORS_ALLOWED_ORIGINS',
201
- 'http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000'
202
  ).split(',')
203
 
204
  CORS_ALLOW_CREDENTIALS = True
 
129
  # Static files (CSS, JavaScript, Images)
130
  # https://docs.djangoproject.com/en/5.2/howto/static-files/
131
 
132
+ STATIC_URL = '/static/'
133
  STATIC_ROOT = BASE_DIR / 'static'
134
 
135
+ MEDIA_URL = '/media/'
136
  MEDIA_ROOT = BASE_DIR / 'media'
137
 
138
  # Default primary key field type
 
198
  # CORS Configuration
199
  CORS_ALLOWED_ORIGINS = os.environ.get(
200
  'CORS_ALLOWED_ORIGINS',
201
+ 'http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000,http://127.0.0.1:5173'
202
  ).split(',')
203
 
204
  CORS_ALLOW_CREDENTIALS = True
api/admin.py CHANGED
@@ -1,7 +1,16 @@
1
  from django.contrib import admin
2
  from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
3
  from django.utils.html import format_html
4
- from .models import User, Product, Transaction, Budget, Ad
 
 
 
 
 
 
 
 
 
5
 
6
 
7
  @admin.register(User)
 
1
  from django.contrib import admin
2
  from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
3
  from django.utils.html import format_html
4
+ from .models import User, Product, Transaction, Budget, Ad, AIInsight
5
+
6
+
7
+ @admin.register(AIInsight)
8
+ class AIInsightAdmin(admin.ModelAdmin):
9
+ """Administration des insights IA"""
10
+ list_display = ['user', 'created_at', 'context_hash']
11
+ list_filter = ['created_at', 'user']
12
+ search_fields = ['user__email', 'content']
13
+ readonly_fields = ['created_at', 'context_hash']
14
 
15
 
16
  @admin.register(User)
api/gemini_service.py CHANGED
@@ -26,7 +26,7 @@ class GeminiService:
26
 
27
  self.client = genai.Client(api_key=self.api_key)
28
  # Use free tier model with generous quotas
29
- self.model = "gemini-1.5-flash" # Free tier: 15 RPM, 1M tokens/day
30
 
31
  def process_voice_command(self, audio_bytes, mime_type="audio/mp3"):
32
  """
@@ -85,6 +85,7 @@ class GeminiService:
85
  )
86
 
87
  result = json.loads(response.text)
 
88
  return result
89
 
90
  except Exception as e:
@@ -94,3 +95,123 @@ class GeminiService:
94
  "intent": "error",
95
  "error": str(e)
96
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  self.client = genai.Client(api_key=self.api_key)
28
  # Use free tier model with generous quotas
29
+ self.model = "gemini-flash-latest" # Free tier: 15 RPM, 1M tokens/day
30
 
31
  def process_voice_command(self, audio_bytes, mime_type="audio/mp3"):
32
  """
 
85
  )
86
 
87
  result = json.loads(response.text)
88
+ print(f"Gemini AI Voice Result: {result}")
89
  return result
90
 
91
  except Exception as e:
 
95
  "intent": "error",
96
  "error": str(e)
97
  }
98
+
99
+ def process_text_command(self, text):
100
+ """
101
+ Process text input to extract transaction details.
102
+ """
103
+ prompt = """
104
+ You are an AI assistant for a financial app called Akompta.
105
+ Your task is to analyze the user's text command and extract transaction details.
106
+
107
+ The user might say things like:
108
+ - "J'ai vendu la tomate pour 500FCFA le Kilo" (Income)
109
+ - "J'ai payé un ordinateur à 300000FCFA" (Expense)
110
+ - "Ajoute un produit Tomate à 200FCFA le bol, j'en ai 30 en stock" (Create Product)
111
+
112
+ Please perform the following:
113
+ 1. Analyze the intent and extract structured data.
114
+
115
+ Return ONLY a JSON object with the following structure:
116
+
117
+ For transactions:
118
+ {
119
+ "transcription": "The input text",
120
+ "intent": "create_transaction",
121
+ "data": {
122
+ "type": "income" or "expense",
123
+ "amount": number,
124
+ "currency": "FCFA",
125
+ "category": "Category name",
126
+ "name": "Description",
127
+ "date": "YYYY-MM-DD"
128
+ }
129
+ }
130
+
131
+ For products/inventory:
132
+ {
133
+ "transcription": "The input text",
134
+ "intent": "create_product",
135
+ "data": {
136
+ "name": "Product name",
137
+ "price": number,
138
+ "unit": "Unit (e.g. bol, kg, unit)",
139
+ "description": "Short description",
140
+ "category": "vente" or "depense" or "stock",
141
+ "stock_status": "ok" or "low" or "rupture"
142
+ }
143
+ }
144
+
145
+ If the text is not clear, return:
146
+ {
147
+ "transcription": "...",
148
+ "intent": "unknown",
149
+ "error": "Reason"
150
+ }
151
+ """
152
+
153
+ try:
154
+ response = self.client.models.generate_content(
155
+ model=self.model,
156
+ contents=[
157
+ types.Content(
158
+ parts=[
159
+ types.Part.from_text(text=f"User command: {text}"),
160
+ types.Part.from_text(text=prompt)
161
+ ]
162
+ )
163
+ ],
164
+ config=types.GenerateContentConfig(
165
+ response_mime_type="application/json"
166
+ )
167
+ )
168
+
169
+ result = json.loads(response.text)
170
+ print(f"Gemini AI Result: {result}")
171
+ # Ensure transcription is the input text if not provided by AI
172
+ if not result.get('transcription'):
173
+ result['transcription'] = text
174
+ return result
175
+
176
+ except Exception as e:
177
+ print(f"Error calling Gemini: {e}")
178
+ return {
179
+ "transcription": text,
180
+ "intent": "error",
181
+ "error": str(e)
182
+ }
183
+
184
+ def process_insights(self, context_data):
185
+ """
186
+ Generate financial insights based on context data.
187
+ """
188
+ prompt = f"""
189
+ Tu es un analyste financier expert pour l'application Akompta.
190
+ Analyse les données suivantes (JSON) :
191
+ {json.dumps(context_data)}
192
+
193
+ Génère exactement 3 insights courts et percutants (max 1 phrase chacun) en Français :
194
+ 1. Une observation sur les ventes ou revenus.
195
+ 2. Une observation sur les dépenses.
196
+ 3. Une alerte sur le stock ou une recommandation.
197
+
198
+ Format de réponse attendu : Une liste simple de 3 phrases séparées par des sauts de ligne. Pas de markdown complexe, pas de titres.
199
+ """
200
+
201
+ try:
202
+ response = self.client.models.generate_content(
203
+ model=self.model,
204
+ contents=prompt
205
+ )
206
+
207
+ text = response.text or ""
208
+ items = [line.strip() for line in text.split('\n') if line.strip()]
209
+ return items[:3]
210
+
211
+ except Exception as e:
212
+ print(f"Error calling Gemini for insights: {e}")
213
+ return [
214
+ "Analyse des ventes en cours...",
215
+ "Vérification des stocks...",
216
+ "Calcul des marges..."
217
+ ]
api/migrations/0004_aiinsight.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.8 on 2026-01-22 20:50
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('api', '0003_notification_supportticket'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='AIInsight',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('content', models.JSONField()),
20
+ ('context_hash', models.CharField(max_length=64)),
21
+ ('created_at', models.DateTimeField(auto_now_add=True)),
22
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ai_insights', to=settings.AUTH_USER_MODEL)),
23
+ ],
24
+ options={
25
+ 'verbose_name': 'Insight IA',
26
+ 'verbose_name_plural': 'Insights IA',
27
+ 'ordering': ['-created_at'],
28
+ },
29
+ ),
30
+ ]
api/models.py CHANGED
@@ -278,4 +278,20 @@ class SupportTicket(models.Model):
278
 
279
  def __str__(self):
280
  return f"{self.subject} - {self.status}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
 
278
 
279
  def __str__(self):
280
  return f"{self.subject} - {self.status}"
281
+
282
+
283
+ class AIInsight(models.Model):
284
+ """Modèle pour stocker les insights générés par l'IA"""
285
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ai_insights')
286
+ content = models.JSONField() # Liste de phrases
287
+ context_hash = models.CharField(max_length=64) # Hash des données utilisées pour la génération
288
+ created_at = models.DateTimeField(auto_now_add=True)
289
+
290
+ class Meta:
291
+ verbose_name = "Insight IA"
292
+ verbose_name_plural = "Insights IA"
293
+ ordering = ['-created_at']
294
+
295
+ def __str__(self):
296
+ return f"Insight pour {self.user.email} - {self.created_at}"
297
 
api/serializers.py CHANGED
@@ -208,8 +208,23 @@ class KPISerializer(serializers.Serializer):
208
  """Serializer pour les KPIs"""
209
 
210
  average_basket = serializers.DecimalField(max_digits=15, decimal_places=2)
 
211
  estimated_mrr = serializers.DecimalField(max_digits=15, decimal_places=2)
 
212
  cac = serializers.DecimalField(max_digits=15, decimal_places=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
 
215
  class NotificationSerializer(serializers.ModelSerializer):
 
208
  """Serializer pour les KPIs"""
209
 
210
  average_basket = serializers.DecimalField(max_digits=15, decimal_places=2)
211
+ average_basket_growth = serializers.FloatField(default=0.0)
212
  estimated_mrr = serializers.DecimalField(max_digits=15, decimal_places=2)
213
+ estimated_mrr_growth = serializers.FloatField(default=0.0)
214
  cac = serializers.DecimalField(max_digits=15, decimal_places=2)
215
+ cac_growth = serializers.FloatField(default=0.0)
216
+
217
+
218
+ class ActivityAnalyticsSerializer(serializers.Serializer):
219
+ """Serializer pour l'activité hebdomadaire"""
220
+ day = serializers.CharField()
221
+ sales = serializers.DecimalField(max_digits=15, decimal_places=2)
222
+
223
+
224
+ class BalanceHistorySerializer(serializers.Serializer):
225
+ """Serializer pour l'historique du solde"""
226
+ date = serializers.CharField()
227
+ balance = serializers.DecimalField(max_digits=15, decimal_places=2)
228
 
229
 
230
  class NotificationSerializer(serializers.ModelSerializer):
api/urls.py CHANGED
@@ -5,8 +5,9 @@ from rest_framework_simplejwt.views import TokenRefreshView
5
  from .views import (
6
  RegisterView, LoginView, ProfileView, ChangePasswordView,
7
  ProductViewSet, TransactionViewSet, BudgetViewSet, AdViewSet,
8
- NotificationViewSet, SupportTicketViewSet, VoiceCommandView,
9
- analytics_overview, analytics_breakdown, analytics_kpi
 
10
  )
11
 
12
  # Router pour les ViewSets
@@ -30,10 +31,13 @@ urlpatterns = [
30
  path('analytics/overview/', analytics_overview, name='analytics-overview'),
31
  path('analytics/breakdown/', analytics_breakdown, name='analytics-breakdown'),
32
  path('analytics/kpi/', analytics_kpi, name='analytics-kpi'),
 
 
33
 
34
  # ===== ROUTER (Products, Transactions, Budgets, Ads) =====
35
  path('', include(router.urls)),
36
 
37
  # ===== VOICE AI =====
38
  path('voice-command/', VoiceCommandView.as_view(), name='voice-command'),
 
39
  ]
 
5
  from .views import (
6
  RegisterView, LoginView, ProfileView, ChangePasswordView,
7
  ProductViewSet, TransactionViewSet, BudgetViewSet, AdViewSet,
8
+ NotificationViewSet, SupportTicketViewSet, VoiceCommandView, AIInsightsView,
9
+ analytics_overview, analytics_breakdown, analytics_kpi, analytics_activity,
10
+ analytics_balance_history
11
  )
12
 
13
  # Router pour les ViewSets
 
31
  path('analytics/overview/', analytics_overview, name='analytics-overview'),
32
  path('analytics/breakdown/', analytics_breakdown, name='analytics-breakdown'),
33
  path('analytics/kpi/', analytics_kpi, name='analytics-kpi'),
34
+ path('analytics/activity/', analytics_activity, name='analytics-activity'),
35
+ path('analytics/balance-history/', analytics_balance_history, name='analytics-balance-history'),
36
 
37
  # ===== ROUTER (Products, Transactions, Budgets, Ads) =====
38
  path('', include(router.urls)),
39
 
40
  # ===== VOICE AI =====
41
  path('voice-command/', VoiceCommandView.as_view(), name='voice-command'),
42
+ path('ai-insights/', AIInsightsView.as_view(), name='ai-insights'),
43
  ]
api/views.py CHANGED
@@ -11,15 +11,17 @@ from datetime import timedelta, datetime
11
  from decimal import Decimal
12
  from django_filters.rest_framework import DjangoFilterBackend
13
  import csv
 
 
14
  from django.http import HttpResponse
15
 
16
- from .models import Product, Transaction, Budget, Ad, Notification, SupportTicket
17
  from .serializers import (
18
  UserSerializer, RegisterSerializer, ChangePasswordSerializer,
19
  ProductSerializer, TransactionSerializer, TransactionSummarySerializer,
20
  BudgetSerializer, AdSerializer, OverviewAnalyticsSerializer,
21
- BreakdownAnalyticsSerializer, KPISerializer, NotificationSerializer,
22
- SupportTicketSerializer
23
  )
24
  from .gemini_service import GeminiService
25
  import tempfile
@@ -41,7 +43,7 @@ class RegisterView(APIView):
41
  refresh = RefreshToken.for_user(user)
42
 
43
  return Response({
44
- 'user': UserSerializer(user).data,
45
  'tokens': {
46
  'refresh': str(refresh),
47
  'access': str(refresh.access_token),
@@ -100,7 +102,7 @@ class LoginView(APIView):
100
  refresh = RefreshToken.for_user(user)
101
 
102
  return Response({
103
- 'user': UserSerializer(user).data,
104
  'tokens': {
105
  'refresh': str(refresh),
106
  'access': str(refresh.access_token),
@@ -113,14 +115,15 @@ class ProfileView(APIView):
113
  permission_classes = [IsAuthenticated]
114
 
115
  def get(self, request):
116
- serializer = UserSerializer(request.user)
117
  return Response(serializer.data)
118
 
119
  def patch(self, request):
120
  serializer = UserSerializer(
121
  request.user,
122
  data=request.data,
123
- partial=True
 
124
  )
125
  if serializer.is_valid():
126
  serializer.save()
@@ -369,49 +372,109 @@ class AnalyticsView(APIView):
369
  return Response(serializer.data)
370
 
371
  def get_kpi(self, request):
372
- """KPIs clés"""
373
  user = request.user
374
  now = timezone.now()
375
  month_ago = now - timedelta(days=30)
 
376
 
377
- # Panier moyen (revenus / nombre de transactions de revenus)
378
- income_transactions = Transaction.objects.filter(
379
- user=user,
380
- type='income',
381
- date__gte=month_ago
382
  )
 
 
 
383
 
384
- total_income = income_transactions.aggregate(
385
- total=Sum('amount')
386
- )['total'] or Decimal('0.00')
387
-
388
- count_income = income_transactions.count()
389
- average_basket = total_income / count_income if count_income > 0 else Decimal('0.00')
390
-
391
- # MRR estimé (revenus du dernier mois)
392
- estimated_mrr = total_income
393
 
394
- # CAC (estimation simplifiée: dépenses marketing / nouveaux clients)
395
- # Pour simplifier, on utilise les dépenses de catégorie "Marketing"
396
- marketing_expenses = Transaction.objects.filter(
397
- user=user,
398
- type='expense',
399
- category__icontains='marketing',
400
- date__gte=month_ago
401
- ).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
402
 
403
- # Estimation simplifiée du CAC
404
- cac = marketing_expenses
 
405
 
 
 
 
 
 
406
  data = {
407
- 'average_basket': average_basket,
408
- 'estimated_mrr': estimated_mrr,
409
- 'cac': cac
 
 
 
410
  }
411
 
412
  serializer = KPISerializer(data)
413
  return Response(serializer.data)
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
  @api_view(['GET'])
417
  @permission_classes([IsAuthenticated])
@@ -434,6 +497,20 @@ def analytics_kpi(request):
434
  return view.get_kpi(request)
435
 
436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  # ========== BUDGETS ==========
438
 
439
  class BudgetViewSet(viewsets.ModelViewSet):
@@ -512,20 +589,27 @@ class VoiceCommandView(APIView):
512
  permission_classes = [IsAuthenticated]
513
 
514
  def post(self, request):
515
- if 'audio' not in request.FILES:
516
- return Response({'error': 'No audio file provided'}, status=status.HTTP_400_BAD_REQUEST)
517
-
518
- audio_file = request.FILES['audio']
519
 
520
- try:
521
- audio_bytes = audio_file.read()
522
- mime_type = audio_file.content_type or 'audio/mp3'
523
 
 
524
  service = GeminiService()
525
- result = service.process_voice_command(audio_bytes, mime_type)
 
 
 
 
 
 
 
 
526
 
527
  if result.get('intent') == 'create_transaction':
528
  data = result.get('data', {})
 
529
 
530
  # Prepare data for serializer
531
  transaction_data = {
@@ -542,6 +626,7 @@ class VoiceCommandView(APIView):
542
  serializer = TransactionSerializer(data=transaction_data, context={'request': request})
543
 
544
  if serializer.is_valid():
 
545
  serializer.save()
546
  return Response({
547
  'status': 'success',
@@ -549,12 +634,48 @@ class VoiceCommandView(APIView):
549
  'transaction': serializer.data
550
  })
551
  else:
 
552
  return Response({
553
  'status': 'error',
554
  'transcription': result.get('transcription'),
555
  'message': 'Validation failed',
556
  'errors': serializer.errors
557
  }, status=status.HTTP_400_BAD_REQUEST)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
  return Response({
560
  'status': 'processed',
@@ -565,4 +686,45 @@ class VoiceCommandView(APIView):
565
  })
566
 
567
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
11
  from decimal import Decimal
12
  from django_filters.rest_framework import DjangoFilterBackend
13
  import csv
14
+ import hashlib
15
+ import json
16
  from django.http import HttpResponse
17
 
18
+ from .models import Product, Transaction, Budget, Ad, Notification, SupportTicket, AIInsight
19
  from .serializers import (
20
  UserSerializer, RegisterSerializer, ChangePasswordSerializer,
21
  ProductSerializer, TransactionSerializer, TransactionSummarySerializer,
22
  BudgetSerializer, AdSerializer, OverviewAnalyticsSerializer,
23
+ BreakdownAnalyticsSerializer, KPISerializer, ActivityAnalyticsSerializer,
24
+ BalanceHistorySerializer, NotificationSerializer, SupportTicketSerializer
25
  )
26
  from .gemini_service import GeminiService
27
  import tempfile
 
43
  refresh = RefreshToken.for_user(user)
44
 
45
  return Response({
46
+ 'user': UserSerializer(user, context={'request': request}).data,
47
  'tokens': {
48
  'refresh': str(refresh),
49
  'access': str(refresh.access_token),
 
102
  refresh = RefreshToken.for_user(user)
103
 
104
  return Response({
105
+ 'user': UserSerializer(user, context={'request': request}).data,
106
  'tokens': {
107
  'refresh': str(refresh),
108
  'access': str(refresh.access_token),
 
115
  permission_classes = [IsAuthenticated]
116
 
117
  def get(self, request):
118
+ serializer = UserSerializer(request.user, context={'request': request})
119
  return Response(serializer.data)
120
 
121
  def patch(self, request):
122
  serializer = UserSerializer(
123
  request.user,
124
  data=request.data,
125
+ partial=True,
126
+ context={'request': request}
127
  )
128
  if serializer.is_valid():
129
  serializer.save()
 
372
  return Response(serializer.data)
373
 
374
  def get_kpi(self, request):
375
+ """KPIs clés avec calcul de croissance"""
376
  user = request.user
377
  now = timezone.now()
378
  month_ago = now - timedelta(days=30)
379
+ two_months_ago = now - timedelta(days=60)
380
 
381
+ # --- Période Actuelle (30 derniers jours) ---
382
+ current_income_tx = Transaction.objects.filter(
383
+ user=user, type='income', date__gte=month_ago
 
 
384
  )
385
+ current_total_income = current_income_tx.aggregate(Sum('amount'))['amount__sum'] or Decimal('0.00')
386
+ current_count_income = current_income_tx.count()
387
+ current_avg_basket = current_total_income / current_count_income if current_count_income > 0 else Decimal('0.00')
388
 
389
+ current_marketing = Transaction.objects.filter(
390
+ user=user, type='expense', category__icontains='marketing', date__gte=month_ago
391
+ ).aggregate(Sum('amount'))['amount__sum'] or Decimal('0.00')
 
 
 
 
 
 
392
 
393
+ # --- Période Précédente (30 à 60 jours) ---
394
+ prev_income_tx = Transaction.objects.filter(
395
+ user=user, type='income', date__gte=two_months_ago, date__lt=month_ago
396
+ )
397
+ prev_total_income = prev_income_tx.aggregate(Sum('amount'))['amount__sum'] or Decimal('0.00')
398
+ prev_count_income = prev_income_tx.count()
399
+ prev_avg_basket = prev_total_income / prev_count_income if prev_count_income > 0 else Decimal('0.00')
 
400
 
401
+ prev_marketing = Transaction.objects.filter(
402
+ user=user, type='expense', category__icontains='marketing', date__gte=two_months_ago, date__lt=month_ago
403
+ ).aggregate(Sum('amount'))['amount__sum'] or Decimal('0.00')
404
 
405
+ # --- Calcul des Croissances ---
406
+ def calc_growth(current, prev):
407
+ if prev == 0: return 100.0 if current > 0 else 0.0
408
+ return float(((current - prev) / prev) * 100)
409
+
410
  data = {
411
+ 'average_basket': current_avg_basket,
412
+ 'average_basket_growth': calc_growth(current_avg_basket, prev_avg_basket),
413
+ 'estimated_mrr': current_total_income,
414
+ 'estimated_mrr_growth': calc_growth(current_total_income, prev_total_income),
415
+ 'cac': current_marketing,
416
+ 'cac_growth': calc_growth(current_marketing, prev_marketing)
417
  }
418
 
419
  serializer = KPISerializer(data)
420
  return Response(serializer.data)
421
 
422
+ def get_activity(self, request):
423
+ """Graphique d'activité: Ventes des 7 derniers jours"""
424
+ user = request.user
425
+ now = timezone.now().date()
426
+ days = []
427
+
428
+ # Récupérer les 7 derniers jours
429
+ for i in range(6, -1, -1):
430
+ day = now - timedelta(days=i)
431
+ total_sales = Transaction.objects.filter(
432
+ user=user,
433
+ type='income',
434
+ date=day
435
+ ).aggregate(Sum('amount'))['amount__sum'] or Decimal('0.00')
436
+
437
+ days.append({
438
+ 'day': day.strftime('%a'), # Lun, Mar, etc.
439
+ 'sales': total_sales
440
+ })
441
+
442
+ serializer = ActivityAnalyticsSerializer(days, many=True)
443
+ return Response(serializer.data)
444
+
445
+ def get_balance_history(self, request):
446
+ """Historique du solde cumulé"""
447
+ user = request.user
448
+ # Récupérer toutes les transactions triées par date
449
+ transactions = Transaction.objects.filter(user=user).order_by('date')
450
+
451
+ history = []
452
+ running_balance = Decimal('0.00')
453
+
454
+ # Grouper par date pour éviter d'avoir trop de points si plusieurs transactions le même jour
455
+ daily_balances = {}
456
+ for t in transactions:
457
+ if t.type == 'income':
458
+ running_balance += t.amount
459
+ else:
460
+ running_balance -= t.amount
461
+
462
+ daily_balances[t.date] = running_balance
463
+
464
+ # Formater pour le frontend
465
+ for date in sorted(daily_balances.keys()):
466
+ history.append({
467
+ 'date': date.strftime('%d/%m'),
468
+ 'balance': daily_balances[date]
469
+ })
470
+
471
+ # Si pas de transactions, ajouter un point à zéro
472
+ if not history:
473
+ history.append({'date': timezone.now().strftime('%d/%m'), 'balance': Decimal('0.00')})
474
+
475
+ serializer = BalanceHistorySerializer(history, many=True)
476
+ return Response(serializer.data)
477
+
478
 
479
  @api_view(['GET'])
480
  @permission_classes([IsAuthenticated])
 
497
  return view.get_kpi(request)
498
 
499
 
500
+ @api_view(['GET'])
501
+ @permission_classes([IsAuthenticated])
502
+ def analytics_activity(request):
503
+ view = AnalyticsView()
504
+ return view.get_activity(request)
505
+
506
+
507
+ @api_view(['GET'])
508
+ @permission_classes([IsAuthenticated])
509
+ def analytics_balance_history(request):
510
+ view = AnalyticsView()
511
+ return view.get_balance_history(request)
512
+
513
+
514
  # ========== BUDGETS ==========
515
 
516
  class BudgetViewSet(viewsets.ModelViewSet):
 
589
  permission_classes = [IsAuthenticated]
590
 
591
  def post(self, request):
592
+ audio_file = request.FILES.get('audio')
593
+ text_command = request.data.get('text')
 
 
594
 
595
+ if not audio_file and not text_command:
596
+ return Response({'error': 'No audio file or text command provided'}, status=status.HTTP_400_BAD_REQUEST)
 
597
 
598
+ try:
599
  service = GeminiService()
600
+
601
+ if audio_file:
602
+ audio_bytes = audio_file.read()
603
+ mime_type = audio_file.content_type or 'audio/mp3'
604
+ result = service.process_voice_command(audio_bytes, mime_type)
605
+ else:
606
+ result = service.process_text_command(text_command)
607
+
608
+ print(f"VoiceCommandView - Result Intent: {result.get('intent')}")
609
 
610
  if result.get('intent') == 'create_transaction':
611
  data = result.get('data', {})
612
+ print(f"VoiceCommandView - Transaction Data: {data}")
613
 
614
  # Prepare data for serializer
615
  transaction_data = {
 
626
  serializer = TransactionSerializer(data=transaction_data, context={'request': request})
627
 
628
  if serializer.is_valid():
629
+ print("VoiceCommandView - Serializer is valid. Saving...")
630
  serializer.save()
631
  return Response({
632
  'status': 'success',
 
634
  'transaction': serializer.data
635
  })
636
  else:
637
+ print(f"VoiceCommandView - Serializer Errors: {serializer.errors}")
638
  return Response({
639
  'status': 'error',
640
  'transcription': result.get('transcription'),
641
  'message': 'Validation failed',
642
  'errors': serializer.errors
643
  }, status=status.HTTP_400_BAD_REQUEST)
644
+
645
+ elif result.get('intent') == 'create_product':
646
+ data = result.get('data', {})
647
+ print(f"VoiceCommandView - Product Data: {data}")
648
+
649
+ product_data = {
650
+ 'name': data.get('name'),
651
+ 'price': data.get('price'),
652
+ 'unit': data.get('unit') or 'unité',
653
+ 'description': data.get('description') or '',
654
+ 'category': data.get('category') or 'stock',
655
+ 'stock_status': data.get('stock_status') or 'ok'
656
+ }
657
+
658
+ # Map common AI terms to valid choices if needed
659
+ if product_data['stock_status'] == 'instock': product_data['stock_status'] = 'ok'
660
+ if product_data['stock_status'] == 'outofstock': product_data['stock_status'] = 'rupture'
661
+
662
+ serializer = ProductSerializer(data=product_data, context={'request': request})
663
+ if serializer.is_valid():
664
+ print("VoiceCommandView - Product Serializer is valid. Saving...")
665
+ serializer.save()
666
+ return Response({
667
+ 'status': 'success',
668
+ 'transcription': result.get('transcription'),
669
+ 'product': serializer.data
670
+ })
671
+ else:
672
+ print(f"VoiceCommandView - Product Serializer Errors: {serializer.errors}")
673
+ return Response({
674
+ 'status': 'error',
675
+ 'transcription': result.get('transcription'),
676
+ 'message': 'Product validation failed',
677
+ 'errors': serializer.errors
678
+ }, status=status.HTTP_400_BAD_REQUEST)
679
 
680
  return Response({
681
  'status': 'processed',
 
686
  })
687
 
688
  except Exception as e:
689
+ return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
690
+
691
+
692
+ class AIInsightsView(APIView):
693
+ """Génération d'insights financiers via Gemini avec mise en mémoire en base de données"""
694
+ permission_classes = [IsAuthenticated]
695
+
696
+ def post(self, request):
697
+ context_data = request.data.get('context', {})
698
+
699
+ # Calculer un hash du contexte pour détecter les changements
700
+ context_str = json.dumps(context_data, sort_keys=True)
701
+ context_hash = hashlib.sha256(context_str.encode()).hexdigest()
702
+
703
+ # Vérifier si un insight existe déjà pour ce contexte et cet utilisateur
704
+ existing_insight = AIInsight.objects.filter(
705
+ user=request.user,
706
+ context_hash=context_hash
707
+ ).first()
708
+
709
+ if existing_insight:
710
+ return Response({'insights': existing_insight.content, 'cached': True})
711
+
712
+ try:
713
+ service = GeminiService()
714
+ insights = service.process_insights(context_data)
715
+
716
+ # Sauvegarder le nouvel insight
717
+ AIInsight.objects.create(
718
+ user=request.user,
719
+ content=insights,
720
+ context_hash=context_hash
721
+ )
722
+
723
+ return Response({'insights': insights, 'cached': False})
724
+ except Exception as e:
725
+ # En cas d'erreur de l'IA, essayer de renvoyer le dernier insight connu
726
+ last_insight = AIInsight.objects.filter(user=request.user).first()
727
+ if last_insight:
728
+ return Response({'insights': last_insight.content, 'cached': True, 'error_fallback': str(e)})
729
+
730
  return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)