Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VITE_API_BASE_URL=http://localhost:1338
VITE_API_BASE_URL=http://127.0.0.1:1338
# VITE_API_BASE_URL=https://giving-cabbage-d8e17e469b.strapiapp.com

VITE_API_REGISTER=/api/auth/local/register
Expand Down Expand Up @@ -47,4 +47,5 @@ VITE_RECAPATCH=6LfLqG0sAAAAAIVBgjd_WSzhhcbIA8Z_boHtbtUv
VITE_VAPID_PUBLIC_KEY=BJmu1K4k02ru__di6cZa40gJD1b8wRy_K2oivy6LIzAf-EMBYl3u2fjTH1yUKp2bp2YZIPqhE0A0GsaczxqAJ9A


VITE_CMS_ANALYTICS_ENDPOINT=/api/cms-analytics
VITE_CMS_ANALYTICS_ENDPOINT=/api/cms-analytics
VITE_LISTEN_ALL=true
75 changes: 75 additions & 0 deletions src/domain/useCase/useAdminWallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState, useCallback, useMemo } from 'react';
import { WalletRepository } from '@infrastructure/repository/WalletRepository';

export const useAdminWallet = () => {
const [platformWallet, setPlatformWallet] = useState(null);
const [allWallets, setAllWallets] = useState([]);
const [walletsMeta, setWalletsMeta] = useState(null);

const [loadingPlatform, setLoadingPlatform] = useState(false);
const [loadingAll, setLoadingAll] = useState(false);
const [updatingCommission, setUpdatingCommission] = useState(false);

const [error, setError] = useState(null);

const repository = useMemo(() => new WalletRepository(), []);

const fetchPlatformWallet = useCallback(async () => {
setLoadingPlatform(true);
setError(null);
try {
const data = await repository.getPlatformWallet();
setPlatformWallet(data);
} catch (err) {
setError(err.response?.data?.error?.message || 'Failed to fetch platform wallet');
} finally {
setLoadingPlatform(false);
}
}, [repository]);

const fetchAllWallets = useCallback(async (params = { page: 1, pageSize: 25 }) => {
setLoadingAll(true);
setError(null);
try {
const result = await repository.getAllWallets(params);
setAllWallets(result.data);
setWalletsMeta(result.meta);
} catch (err) {
setError(err.response?.data?.error?.message || 'Failed to fetch all wallets');
} finally {
setLoadingAll(false);
}
}, [repository]);

const updateCommission = useCallback(async (walletId, newRate) => {
setUpdatingCommission(true);
setError(null);
try {
await repository.updateCommissionRate(walletId, newRate);
// After successful update, re-fetch the list to show updated values
await fetchAllWallets();
return true;
} catch (err) {
const msg = err.response?.data?.error?.message || 'Failed to update commission rate';
setError(msg);
throw new Error(msg);
} finally {
setUpdatingCommission(false);
}
}, [repository, fetchAllWallets]);

return {
platformWallet,
allWallets,
walletsMeta,

loadingPlatform,
loadingAll,
updatingCommission,
error,

fetchPlatformWallet,
fetchAllWallets,
updateCommission
};
};
45 changes: 45 additions & 0 deletions src/domain/useCase/useFetchAdminPayouts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { PayoutAdminRepository } from '../../infrastructure/repository/PayoutAdminRepository';
import { useMemo, useEffect, useCallback, useState } from 'react';

/**
* UseCase hook for fetching and managing payouts (Admin/CMS view).
* Supports server-side pagination & search.
*/
export const useFetchAdminPayouts = () => {
const repository = useMemo(() => new PayoutAdminRepository(), []);
const [statusFilter, setStatusFilter] = useState(null);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const pageSize = 10;

const fetchPayouts = useCallback(async () => {
return await repository.getAll(statusFilter, page, pageSize, search);
}, [repository, statusFilter, page, search]);

const { execute, returnedData, inProgress, error } = useAsyncUseCase(fetchPayouts);

useEffect(() => {
execute();
}, [execute]);

const updateStatus = useCallback(async (id, status, adminNotes = '') => {
await repository.updateStatus(id, status, adminNotes);
await execute(); // Refresh list
}, [repository, execute]);

return {
payouts: returnedData?.items || [],
totalItems: returnedData?.meta?.pagination?.total || 0,
totalPages: Math.max(1, Math.ceil((returnedData?.meta?.pagination?.total || 0) / pageSize)),
currentPage: page,
setPage,
setSearch,
isLoading: inProgress,
error,
fetch: execute,
updateStatus,
statusFilter,
setStatusFilter
};
};
25 changes: 25 additions & 0 deletions src/domain/useCase/useInitiatePayment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState, useCallback } from 'react';
import { PaymentRepository } from '@infrastructure/repository/PaymentRepository';

export const useInitiatePayment = () => {
const [inProgress, setInProgress] = useState(false);
const [error, setError] = useState(null);
const paymentRepo = new PaymentRepository();

const initiatePayment = useCallback(async (itemId, contentType) => {
setInProgress(true);
setError(null);
try {
const data = await paymentRepo.initiateCheckout(itemId, contentType);
return data;
} catch (err) {
const message = err.response?.data?.error?.message || 'Failed to initiate payment. Please try again.';
setError(message);
throw err;
} finally {
setInProgress(false);
}
}, [paymentRepo]);

return { initiatePayment, inProgress, error };
};
50 changes: 50 additions & 0 deletions src/domain/useCase/useWallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState, useCallback, useMemo } from 'react';
import { WalletRepository } from '@infrastructure/repository/WalletRepository';

export const useWallet = () => {
const [wallet, setWallet] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [requestingPayout, setRequestingPayout] = useState(false);

const repository = useMemo(() => new WalletRepository(), []);

const fetchWallet = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await repository.getMyWallet();
setWallet(data);
} catch (err) {
setError(err.response?.data?.error?.message || 'Failed to fetch wallet data');
} finally {
setLoading(false);
}
}, [repository]);

const requestPayout = useCallback(async (amount, method, details) => {
setRequestingPayout(true);
setError(null);
try {
const result = await repository.requestPayout({ amount, method, details });
// Refresh wallet to show updated balance
await fetchWallet();
return result;
} catch (err) {
const msg = err.response?.data?.error?.message || 'Payout request failed';
setError(msg);
throw new Error(msg);
} finally {
setRequestingPayout(false);
}
}, [repository, fetchWallet]);

return {
wallet,
loading,
error,
requestingPayout,
fetchWallet,
requestPayout
};
};
75 changes: 75 additions & 0 deletions src/infrastructure/DTO/WalletDTO.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Data Transfer Object for Wallet responses
*/
export class WalletDTO {
constructor(data) {
if (!data) return;

this.id = data.id;
this.documentId = data.documentId;
this.ownerType = data.owner_type;
this.balance = Number(data.balance || 0);
this.pendingBalance = Number(data.pending_balance || 0);
this.currency = data.currency || 'EGP';
this.commissionRate = data.commission_rate ? Number(data.commission_rate) : 0;
this.isActive = Boolean(data.is_active);

// Owner info (can be populated)
this.owner = data.owner ? {
id: data.owner.id,
username: data.owner.username,
email: data.owner.email,
} : null;

this.transactions = Array.isArray(data.transactions) ? data.transactions.map(t => new TransactionDTO(t)) : [];
this.payouts = Array.isArray(data.payouts) ? data.payouts.map(p => new PayoutDTO(p)) : [];

this.stats = data.stats || null;

this.createdAt = data.createdAt ? new Date(data.createdAt) : null;
}
}

export class TransactionDTO {
constructor(data) {
if (!data) return;

this.id = data.id;
this.type = data.type; // 'credit' | 'debit' | 'commission' | 'withdrawal' | 'refund'
this.amount = Number(data.amount || 0);
this.description = data.description || '';
this.referenceType = data.reference_type;
this.referenceId = data.reference_id;
this.metadata = data.metadata || {};
this.status = data.status; // 'PENDING' | 'COMPLETED' | 'FAILED' | 'REVERSED'
this.createdAt = data.createdAt ? new Date(data.createdAt) : null;
}
}

export class PayoutDTO {
constructor(data) {
if (!data) return;

this.id = data.id;
this.documentId = data.documentId;
this.amount = Number(data.amount || 0);
this.status = data.status; // 'PENDING' | 'PAID' | 'REJECTED'
this.method = data.method;
this.details = data.details || {};
this.admin_notes = data.admin_notes;
this.createdAt = data.createdAt ? new Date(data.createdAt) : null;

// Populate Wallet owner if available
if (data.wallet) {
this.wallet = {
id: data.wallet.id,
balance: data.wallet.balance,
owner: data.wallet.owner ? {
id: data.wallet.owner.id,
username: data.wallet.owner.username,
email: data.wallet.owner.email
} : null
};
}
}
}
9 changes: 9 additions & 0 deletions src/infrastructure/repository/GlobalTagRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ export class GlobalTagRepository extends BaseRepository {
async deleteTag(id) {
return await this.delete(`${this.endpoint}/${id}`);
}

/**
* Fetches tag audience map: real user interest counts per tag.
* @returns {Promise<{tags: Array, totalUsers: number}>}
*/
async getTagAudience() {
const response = await this.get('/api/recommendations/tag-audience');
return response?.data || { tags: [], totalUsers: 0 };
}
}
28 changes: 28 additions & 0 deletions src/infrastructure/repository/PaymentRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { repositoryRegistry } from './RepositoryRegistry';

export class PaymentRepository {
constructor(apiClient = repositoryRegistry.apiClient) {
this.apiClient = apiClient;
}

/**
* Initiates a checkout process for a specific item (Event or Course)
* @param {string} itemId - The documentId or id of the item
* @param {string} contentType - 'event' or 'course'
* @returns {Promise<{payment_key: string, order_id: string, iframe_url: string}>}
*/
async initiateCheckout(itemId, contentType = 'event') {
try {
// Using BaseRepository.post which handles path prefixing and wrapping
const response = await this.apiClient.post('/api/payments/initiate', {
itemId,
contentType
}, true);

return response.data;
} catch (error) {
console.error('PaymentRepository initiateCheckout failed', error);
throw error;
}
}
}
Loading
Loading