Changes between Initial Version and Version 1 of UseCaseImplementations


Ignore:
Timestamp:
01/01/26 22:29:35 (6 days ago)
Author:
231104
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • UseCaseImplementations

    v1 v1  
     1== Имплементација на случаи на употреба
     2Заклучно со оваа фаза се имплементирани сите предвидени кориснички сценарија, односно:
     3
     4[[Image(p.png)]]
     5
     6== Ненајавен корисник
     7= ИД 1 - Регистрација на корисник
     8Корисникот се наоѓа на home страницата и го кликнува копчето за регистрација на корисник.
     9
     10[[Image(r1.png)]]
     11
     12Притоа, корисникот е пренасочен кон /sign-up каде што внесува потребни информации за регистрација (име, презиме, адреса, град, држава, поштенски код, SSN, датум на раѓање, email, лозинка).
     13
     14[[Image(r2.png)]]
     15
     16= ИД 1
     17За секоја потребна информација чуваме посебен Hook, и тие се ажурираат при секоја промена од страна на корисникот.
     18
     19
     20{{{
     21const form = useForm<z.infer<typeof formSchema>>({
     22    resolver: zodResolver(formSchema),
     23    defaultValues: {
     24        email: '', password: '', firstName: '', lastName: '',
     25        address1: '', city: '', state: '', postalCode: '',
     26        dateOfBirth: '', ssn: '',
     27    },
     28});
     29}}}
     30
     31
     32= ИД 1
     33
     34Во следните неколку слики е опишана обработката на регистрацијата на корисникот.
     35При клик на копчето Sign Up, се испраќа POST барање до backend апликацијата во кој се сместени поднесените информации.
     36{{{
     37const onSubmit = async (data: z.infer<typeof formSchema>) => {
     38    setIsLoading(true);
     39    try {
     40        if (type === 'sign-up') {
     41            await register({
     42                firstName: data.firstName!, lastName: data.lastName!,
     43                email: data.email, password: data.password,
     44                address1: data.address1!, city: data.city!,
     45                state: data.state!, postalCode: data.postalCode!,
     46                dateOfBirth: data.dateOfBirth!, ssn: data.ssn!,
     47            });
     48            setSuccess('Registration successful! Redirecting...');
     49        }
     50    } catch (error) {
     51        setError(error instanceof Error ? error.message : 'Authentication failed');
     52    }
     53};
     54}}}
     55= ИД 1
     56Во backend апликацијата, POST request-от е примен од страна на контролерот register_view кој го содржи соодветен API endpoint, кој ги препраќа потребните информации кон базата.
     57{{{
     58@api_view(['POST'])
     59@permission_classes([AllowAny])
     60def register_view(request):
     61    data = request.data
     62    email = data.get('email')
     63    password = data.get('password')
     64    first_name = data.get('firstName', '')
     65    last_name = data.get('lastName', '')
     66   
     67    user = User.objects.create_user(
     68        username=email, email=email, password=password,
     69        first_name=first_name, last_name=last_name
     70    )
     71   
     72    refresh = RefreshToken.for_user(user)
     73    return Response({
     74        'user': {'id': user.id, 'email': user.email},
     75        'access': str(refresh.access_token),
     76        'refresh': str(refresh)
     77    }, status=status.HTTP_201_CREATED)
     78}}}
     79При успешна регистрација, корисникот е пренасочен кон страницата за поврзување на банкарска сметка преку Plaid.
     80
     81[[Image(r3.png)]]
     82
     83== ИД 2 - Најава на корисник
     84Корисникот се најавува и е редиректиран на home страницата како најавен корисник со дополнителни функции.
     85
     86[[Image(r1.png)]]
     87
     88= ИД 2
     89При клик на копчето Sign In, се испраќа POST барање до backend апликацијата за верификација на креденцијалите.
     90{{{
     91const login = async (email: string, password: string) => {
     92    const response = await fetch('http://localhost:8000/api/auth/login/', {
     93        method: 'POST',
     94        headers: { 'Content-Type': 'application/json' },
     95        body: JSON.stringify({ email, password }),
     96    });
     97   
     98    if (response.ok) {
     99        const data = await response.json();
     100        localStorage.setItem('access_token', data.access);
     101        localStorage.setItem('refresh_token', data.refresh);
     102        setUser(data.user);
     103        router.push('/');
     104    }
     105};
     106}}}
     107= ИД 2
     108Во backend апликацијата, се верификуваат креденцијалите и се генерираат JWT токени.
     109{{{
     110@api_view(['POST'])
     111@permission_classes([AllowAny])
     112def login_view(request):
     113    email = request.data.get('email')
     114    password = request.data.get('password')
     115   
     116    user = authenticate(request, username=email, password=password)
     117   
     118    if user is None:
     119        return Response({'error': 'Invalid credentials'},
     120                       status=status.HTTP_401_UNAUTHORIZED)
     121   
     122    refresh = RefreshToken.for_user(user)
     123    return Response({
     124        'user': {'id': user.id, 'email': user.email},
     125        'access': str(refresh.access_token),
     126        'refresh': str(refresh)
     127    })
     128}}}
     129
     130[[Image(r5.png)]]
     131
     132== Најавен корисник (User)
     133== ИД 3 - Преглед на контролен панел (Home)
     134Корисникот го кликнува копчето Home во навигацијата за преглед на контролниот панел со вкупен баланс, број на сметки и последни трансакции.
     135
     136[[Image(r5.png)]]
     137
     138= ИД 3
     139Се користи custom hook useDashboard за превземање на податоците од backend.
     140{{{
     141export const useDashboard = () => {
     142  const [data, setData] = useState<DashboardData | null>(null);
     143 
     144  const fetchDashboard = async () => {
     145    const accessToken = localStorage.getItem('access_token');
     146    const response = await fetch('http://localhost:8000/api/banking/dashboard/', {
     147      headers: {
     148        'Authorization': `Bearer ${accessToken}`,
     149        'Content-Type': 'application/json',
     150      },
     151    });
     152   
     153    if (response.ok) {
     154      const dashboardData = await response.json();
     155      setData(dashboardData);
     156    }
     157  };
     158 
     159  return { data, loading, error, refetch: fetchDashboard };
     160};
     161}}}
     162= ИД 3
     163Backend API го враќа вкупниот баланс, бројот на сметки и последните трансакции.
     164{{{
     165@api_view(['GET'])
     166@permission_classes([IsAuthenticated])
     167def dashboard_view(request):
     168    user_accounts = Account.objects.filter(user=request.user)
     169    total_balance = sum(account.balance for account in user_accounts)
     170    recent_transactions = Transaction.objects.filter(
     171        account__in=user_accounts
     172    ).order_by('-created_at')[:5]
     173   
     174    return Response({
     175        'total_balance': float(total_balance),
     176        'accounts_count': user_accounts.count(),
     177        'accounts': accounts_data,
     178        'recent_transactions': transactions_data,
     179    })
     180}}}
     181== ИД 4 - Преглед на банкарски сметки (My Banks)
     182Корисникот кликнува на My Banks во навигацијата и ги гледа сите поврзани банкарски сметки како визуелни картички.
     183
     184[[Image(r8.png)]]
     185
     186= ИД 4
     187Компонентата BankCard ги прикажува деталите за секоја банкарска сметка.
     188{{{
     189const BankCard = ({account, userName, showBalance = true}: CreditCardProps) => {
     190  return (
     191    <div className='flex flex-col'>
     192      <Link href='/' className='bank-card relative overflow-hidden'>
     193        <div className='bank-card-content'>
     194          <h1 className='text-18 font-semibold text-white'>
     195            {account.name || userName}
     196          </h1>
     197          <p className='font-bold text-white text-24'>
     198            {formatAmount(account.currentBalance)}
     199          </p>
     200        </div>
     201      </Link>
     202    </div>
     203  )
     204}
     205}}}
     206= ИД 4
     207Backend API ги враќа сите банкарски сметки поврзани со корисникот.
     208{{{
     209@api_view(['GET'])
     210@permission_classes([IsAuthenticated])
     211def accounts_view(request):
     212    user_accounts = Account.objects.filter(user=request.user)
     213    accounts_data = []
     214   
     215    for account in user_accounts:
     216        accounts_data.append({
     217            'id': str(account.id),
     218            'name': f"{account.bank.name} {account.account_type.title()}",
     219            'currentBalance': float(account.balance),
     220            'type': account.account_type,
     221            'account_number': account.account_number,
     222        })
     223   
     224    return Response({'accounts': accounts_data})
     225}}}
     226== ИД 5 - Преглед на историја на трансакции
     227Корисникот кликнува на Transaction History и ја гледа табелата со сите трансакции. Може да филтрира по сметка, статус и категорија.
     228
     229[[Image(r9.png)]]
     230
     231== ИД 5
     232Frontend компонентата овозможува филтрирање и пагинација на трансакциите.
     233{{{
     234const fetchTransactions = async () => {
     235  const params = new URLSearchParams({
     236    page: currentPage.toString(),
     237    per_page: '10',
     238  });
     239 
     240  if (searchQuery) params.append('search', searchQuery);
     241  if (statusFilter !== 'all') params.append('status', statusFilter);
     242  if (categoryFilter !== 'all') params.append('category', categoryFilter);
     243 
     244  const response = await fetch(
     245    `http://localhost:8000/api/banking/transactions/?${params}`,
     246    { headers: { 'Authorization': `Bearer ${accessToken}` } }
     247  );
     248 
     249  const data = await response.json();
     250  setTransactions(data.transactions);
     251  setTotalPages(data.total_pages);
     252};
     253}}}
     254= ИД 5
     255Backend API ги враќа трансакциите со поддршка за филтрирање и пагинација.
     256{{{
     257@api_view(['GET'])
     258@permission_classes([IsAuthenticated])
     259def transactions_view(request):
     260    user_accounts = Account.objects.filter(user=request.user)
     261    transactions_query = Transaction.objects.filter(account__in=user_accounts)
     262   
     263    # Apply filters
     264    if category and category != 'all':
     265        transactions_query = transactions_query.filter(category=category)
     266    if status and status != 'all':
     267        transactions_query = transactions_query.filter(status=status)
     268   
     269    return Response({
     270        'transactions': transactions_data,
     271        'total': total_transactions,
     272        'page': page,
     273        'total_pages': total_pages
     274    })
     275}}}
     276== ИД 6 - Префрлање на средства (Transfer Funds)
     277Корисникот кликнува на Transfer Funds, избира тип на трансфер (Internal/External), изворна и дестинациска сметка, категорија, износ и кликнува Transfer Funds. Трансферот се процесира преку Dwolla API.
     278
     279[[Image(r11.png)]]
     280
     281
     282[[Image(r12.png)]]
     283
     284
     285ИД 6
     286Frontend формата за трансфер користи react-hook-form за валидација.
     287{{{
     288const onSubmit = async (data: TransferFormData) => {
     289  const requestBody = {
     290    source_account_id: data.sourceAccount,
     291    amount: data.amount,
     292    category: data.category,
     293    note: data.note,
     294  };
     295 
     296  if (data.transferType === 'internal') {
     297    requestBody.destination_account_id = data.destinationAccount;
     298  } else {
     299    requestBody.recipient_name = data.recipientName;
     300    requestBody.recipient_email = data.recipientEmail;
     301  }
     302 
     303  const response = await fetch('http://localhost:8000/api/banking/transfer/', {
     304    method: 'POST',
     305    headers: { 'Authorization': `Bearer ${accessToken}` },
     306    body: JSON.stringify(requestBody),
     307  });
     308};
     309}}}
     310= ИД 6
     311Backend API го процесира трансферот и ги ажурира балансите на сметките.
     312{{{
     313@api_view(['POST'])
     314@permission_classes([IsAuthenticated])
     315def transfer_funds(request):
     316    source_account = Account.objects.get(id=source_account_id, user=request.user)
     317   
     318    with transaction.atomic():
     319        Transaction.objects.create(
     320            account=source_account,
     321            transaction_type='debit',
     322            amount=amount,
     323            description=f"Transfer to {destination_account.bank.name}",
     324            category=category,
     325            status='completed'
     326        )
     327       
     328        source_account.balance -= amount
     329        source_account.save()
     330       
     331        destination_account.balance += amount
     332        destination_account.save()
     333   
     334    return Response({'message': 'Transfer completed successfully'})
     335}}}
     336
     337[[Image(r13.png)]]
     338
     339
     340== ИД 7 - Преглед на аналитика (Analytics)
     341Корисникот кликнува на Analytics и ги гледа графиците за Total Income, Total Expenses, Net Savings, pie chart по категории и месечна споредба.
     342
     343[[Image(r14.png)]]
     344
     345
     346= ИД 7
     347Frontend компонентата користи Recharts библиотека за визуелизација на податоците.
     348{{{
     349const Analytics = () => {
     350  const { data, loading } = useAnalytics(dateRange);
     351 
     352  return (
     353    <ResponsiveContainer width="100%" height={300}>
     354      <PieChart>
     355        <Pie
     356          data={data.categoryBreakdown}
     357          dataKey="amount"
     358          label={({ category, percentage }) => `${category}: ${percentage}%`}
     359        >
     360          {data.categoryBreakdown.map((entry, index) => (
     361            <Cell key={index} fill={COLORS[index % COLORS.length]} />
     362          ))}
     363        </Pie>
     364        <Tooltip formatter={(value) => `$${value.toFixed(2)}`} />
     365      </PieChart>
     366    </ResponsiveContainer>
     367  );
     368};
     369}}}
     370= ИД 7
     371Backend API ги пресметува аналитичките податоци од трансакциите.
     372{{{
     373@api_view(['GET'])
     374@permission_classes([IsAuthenticated])
     375def analytics_view(request):
     376    transactions = Transaction.objects.filter(account__in=user_accounts)
     377   
     378    # Category Breakdown
     379    category_data = {}
     380    for txn in transactions:
     381        if txn.transaction_type == 'debit':
     382            category = txn.category or 'other'
     383            category_data[category]['amount'] += float(txn.amount)
     384   
     385    total_income = transactions.filter(transaction_type='credit').aggregate(Sum('amount'))
     386    total_expenses = transactions.filter(transaction_type='debit').aggregate(Sum('amount'))
     387   
     388    return Response({
     389        'categoryBreakdown': category_breakdown,
     390        'incomeVsExpenses': {
     391            'totalIncome': total_income,
     392            'totalExpenses': total_expenses,
     393            'netSavings': total_income - total_expenses
     394        }
     395    })
     396}}}
     397== ИД 8 - Управување со буџети (Budgets)
     398Корисникот кликнува на Budgets и го гледа Budget Manager со вкупно буџетирано, потрошено и останато. Може да креира нови буџети по категории.
     399
     400[[Image(r17.png)]]
     401
     402
     403= ИД 8
     404Budget моделот во Django ги пресметува потрошените средства автоматски.
     405{{{
     406class Budget(models.Model):
     407    user = models.ForeignKey(User, on_delete=models.CASCADE)
     408    category = models.CharField(max_length=100)
     409    amount = models.DecimalField(max_digits=10, decimal_places=2)
     410    month = models.DateField()
     411   
     412    @property
     413    def spent_amount(self):
     414        spent = Transaction.objects.filter(
     415            account__user=self.user,
     416            category__iexact=self.category,
     417            transaction_type='debit',
     418            created_at__gte=self.month
     419        ).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
     420        return float(spent)
     421   
     422    @property
     423    def percentage_used(self):
     424        return round((self.spent_amount / float(self.amount)) * 100, 1)
     425}}}
     426= ИД 8
     427Backend API за креирање на нов буџет.
     428{{{
     429@api_view(['POST'])
     430@permission_classes([IsAuthenticated])
     431def create_budget(request):
     432    category = request.data.get('category')
     433    amount = Decimal(str(request.data.get('amount', 0)))
     434   
     435    budget = Budget.objects.create(
     436        user=request.user,
     437        category=category,
     438        amount=amount,
     439        month=month_date
     440    )
     441   
     442    return Response({
     443        'budget': {
     444            'id': budget.id,
     445            'category': budget.category,
     446            'amount': float(budget.amount),
     447            'spent': budget.spent_amount,
     448            'percentage_used': budget.percentage_used
     449        }
     450    })
     451}}}
     452== ИД 9 - Поврзување на банкарска сметка (Connect Bank)
     453Корисникот кликнува на Connect Bank, ја гледа информацијата за Secure Bank Connection со 256-bit енкрипција, избира банка и кликнува Connect with Plaid.
     454
     455[[Image(r19.png)]]
     456
     457
     458= ИД 9
     459PlaidLink компонентата го иницира процесот на поврзување со Plaid API.
     460{{{
     461const PlaidLink = ({ variant, onSuccess }: PlaidLinkProps) => {
     462  const [linkToken, setLinkToken] = useState<string | null>(null);
     463 
     464  useEffect(() => {
     465    const createLinkToken = async () => {
     466      const response = await fetch(
     467        'http://localhost:8000/api/banking/plaid/create-link-token/',
     468        { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` } }
     469      );
     470      const data = await response.json();
     471      setLinkToken(data.link_token);
     472    };
     473    createLinkToken();
     474  }, []);
     475 
     476  const onPlaidSuccess = useCallback(async (public_token, metadata) => {
     477    await fetch('http://localhost:8000/api/banking/plaid/exchange-token/', {
     478      method: 'POST',
     479      body: JSON.stringify({ public_token }),
     480    });
     481    router.push('/my-banks');
     482  }, []);
     483 
     484  const { open, ready } = usePlaidLink({ token: linkToken, onSuccess: onPlaidSuccess });
     485};
     486}}}
     487= ИД 9
     488Backend API за креирање на Plaid Link Token.
     489{{{
     490@api_view(['POST'])
     491@permission_classes([IsAuthenticated])
     492def create_link_token(request):
     493    link_request = LinkTokenCreateRequest(
     494        user=LinkTokenCreateRequestUser(client_user_id=str(request.user.id)),
     495        client_name='Banker App',
     496        products=[Products('auth'), Products('transactions')],
     497        country_codes=[CountryCode('US')],
     498        language='en'
     499    )
     500    response = client.link_token_create(link_request)
     501    return Response({'link_token': response['link_token']})
     502}}}
     503= ИД 9
     504По успешно поврзување преку Plaid, се креираат банкарски сметки во базата.
     505{{{
     506@api_view(['POST'])
     507@permission_classes([IsAuthenticated])
     508def exchange_public_token(request):
     509    exchange_response = client.item_public_token_exchange(
     510        ItemPublicTokenExchangeRequest(public_token=public_token)
     511    )
     512    access_token = exchange_response['access_token']
     513   
     514    accounts_response = client.accounts_get(AccountsGetRequest(access_token=access_token))
     515   
     516    for plaid_account in accounts_response['accounts']:
     517        account = Account.objects.create(
     518            user=request.user,
     519            bank=bank,
     520            account_type=account_subtype,
     521            balance=Decimal(str(current_balance)),
     522            status='active'
     523        )
     524   
     525    return Response({'message': 'Bank accounts connected via Plaid'})
     526}}}
     527
     528[[Image(r20.png)]]
     529
     530
     531
     532[[Image(r21.png)]]
     533
     534