<?php
namespace App\Controller;
use App\Services\UserService;
use Carbon\Carbon;
use GeoIp2\Record\Location;
use Pimcore\Log\ApplicationLogger;
use Pimcore\Log\Simple;
use Pimcore\Model\DataObject\LocationSource;
use Pimcore\Model\DataObject\Data\ElementMetadata;
use Pimcore\Model\DataObject\MemberStripeAccount;
use Pimcore\Model\DataObject\MembersUser;
use Pimcore\Model\DataObject\Price;
use Pimcore\Model\DataObject\ProductDescription;
use Pimcore\Model\DataObject\Promocode;
use Pimcore\Model\DataObject\PromoRules;
use Pimcore\Model\DataObject\StripeAccount;
use Pimcore\Model\WebsiteSetting;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Stripe;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class StripeController extends AbstractController
{
private CacheInterface $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
protected function validateAdminRole($user): ?Response
{
if ($user === null) {
return new Response(json_encode([
'success' => false,
'data' => 'User not authenticated'
]), Response::HTTP_UNAUTHORIZED);
}
$groups = $user->getGroups();
if (empty($groups) || $groups[0]->getName() !== 'Admin') {
return new Response(json_encode([
'success' => false,
'data' => 'Admin role required'
]), Response::HTTP_FORBIDDEN);
}
return null;
}
private function normalizeAdditionalPriceItem(mixed $item, $membership): ?array
{
$price = null;
$forMembership = null;
if ($item instanceof Price) {
$price = $item;
} elseif ($item instanceof ElementMetadata) {
$element = $item->getElement();
if ($element instanceof Price) {
$price = $element;
}
$forMembership = $item->getForMembership();
}
if (!$price instanceof Price) {
return null;
}
// Include the item for members when it's member-only, or for everyone when not flagged as member-only.
if (($forMembership == 1 && $membership) || $forMembership != 1) {
return [
'id' => $price->getPriceid(),
'title' => $price->getTitle(),
'price' => $price->getPrice() / 100,
];
}
return null;
}
private function freeTools(LocationSource $locationSource): array
{
$data = [];
if ($locationSource instanceof LocationSource) {
$freeTools = $locationSource->getFreeTools();
foreach($freeTools as $tool) {
if ($tool instanceof ProductDescription) {
$data['services'][$tool->getServiceId()] = [
'serviceId' => $tool->getServiceId(),
'title' => $tool->getTitle(),
'price' => 0.0,
'shortDescription' => $tool->getShortDescription(),
'fullDescription' => $tool->getFullDescription(),
'requirements' => $tool->getRequirements(),
'requiredDocuments' => $tool->getRequiredDocuments(),
'estimatedTime' => $tool->getEstimatedTime(),
'required' => $tool->getRequired()
];
}
}
}
return $data;
}
#[Route('/v1/api/user/stripe', name: 'user_stripe', methods: ['GET'])]
public function userStripeAction(Request $request, #[CurrentUser] $user = null): Response
{
$sourceKey = $request->query->get('source');
if ($user === null) {
return new Response(json_encode([
'success' => false,
'data' => 'User not authenticated'
]), Response::HTTP_UNAUTHORIZED);
}
$locationSource = LocationSource::getByObjectKey($sourceKey, 1);
$MemberStripeAccount = new MemberStripeAccount\Listing();
$MemberStripeAccount->setCondition('customerId = ? AND source__id = ?', [
$user->getId(),
$locationSource->getId()
]);
$MemberStripeAccount = $MemberStripeAccount->current();
$data = [];
if (!$MemberStripeAccount instanceof MemberStripeAccount || !$MemberStripeAccount->getRules()) {
$stripeAccount = $locationSource->getStripeAccount();
$attestationPrice = Price::getByPriceid($stripeAccount->getAttestationItem(), 1);
$attestationDescription = $attestationPrice?->getDescription();
$data['services']['attestation'] = [
'id' => $attestationPrice?->getPriceid(),
'title' => $attestationDescription?->getTitle(),
'shortDescription' => $attestationDescription?->getShortDescription(),
'fullDescription' => $attestationDescription?->getFullDescription(),
'requirements' => $attestationDescription?->getRequirements(),
'requiredDocuments' => $attestationDescription?->getRequiredDocuments(),
'estimatedTime' => $attestationDescription?->getEstimatedTime(),
'required' => $attestationDescription?->getRequired(),
'price' => $attestationPrice?->getPrice() / 100,
'serviceId' => $attestationDescription?->getServiceId(),
];
$assistancePrice = Price::getByPriceid($stripeAccount->getAssistanceItem(), 1);
$assistanceDescription = $assistancePrice?->getDescription();
$data['services']['assistance'] = [
'id' => $assistancePrice?->getPriceid(),
'title' => $assistanceDescription?->getTitle(),
'shortDescription' => $assistanceDescription?->getShortDescription(),
'fullDescription' => $assistanceDescription?->getFullDescription(),
'requirements' => $assistanceDescription?->getRequirements(),
'requiredDocuments' => $assistanceDescription?->getRequiredDocuments(),
'estimatedTime' => $assistanceDescription?->getEstimatedTime(),
'required' => $assistanceDescription?->getRequired(),
'price' => $assistancePrice?->getPrice() / 100,
'serviceId' => $assistanceDescription?->getServiceId(),
];
$rentProtectionPrice = Price::getByPriceid($stripeAccount->getInsuranceItem(), 1);
$rentProtectionDescription = $rentProtectionPrice?->getDescription();
$data['services']['rentProtection'] = [
'id' => $rentProtectionPrice?->getPriceid(),
'title' => $rentProtectionDescription?->getTitle(),
'shortDescription' => $rentProtectionDescription?->getShortDescription(),
'fullDescription' => $rentProtectionDescription?->getFullDescription(),
'requirements' => $rentProtectionDescription?->getRequirements(),
'requiredDocuments' => $rentProtectionDescription?->getRequiredDocuments(),
'estimatedTime' => $rentProtectionDescription?->getEstimatedTime(),
'required' => $rentProtectionDescription?->getRequired(),
'price' => $rentProtectionPrice?->getPrice() / 100,
'serviceId' => $rentProtectionDescription?->getServiceId(),
];
}
if ($MemberStripeAccount && $MemberStripeAccount->getRules()){
$rules = PromoRules::getById($MemberStripeAccount->getRules());
}
if ($MemberStripeAccount && $MemberStripeAccount->getRules() && $rules instanceof PromoRules) {
$promos = $rules->getPromo();
if (!empty($promos)) {
foreach ($promos as $promo) {
if ($promo['price']->getData() instanceof Price) {
$price = $promo['price']->getData();
$description = $price->getDescription();
if ($description instanceof ProductDescription) {
$data['services'][$description->getServiceKey()] = [
'id' => $price->getPriceid(),
'title' => $description->getTitle() ? $description->getTitle() : $price->getTitle(),
'shortDescription' => $description->getShortDescription(),
'fullDescription' => $description->getFullDescription(),
'requirements' => $description->getRequirements(),
'requiredDocuments' => $description->getRequiredDocuments(),
'estimatedTime' => $description->getEstimatedTime(),
'required' => $description->getRequired(),
'price' => $price->getPrice() / 100,
'serviceId' => $description->getServiceId(),
];
}
}
}
}
}
$membership = $user->getMembershipForm();
$stripeAccount = StripeAccount::getByAccount($locationSource->getId(), 1);
// Aggiungi servizi aggiuntivi se disponibili
$additionalAttestationItems = $stripeAccount->getAdditionalAttestationItem();
if (!empty($additionalAttestationItems)) {
foreach ($additionalAttestationItems as $item) {
$normalized = $this->normalizeAdditionalPriceItem($item, $membership);
if (!empty($normalized)) {
$data['services']['attestation']['additional'][] = $normalized;
}
}
}
$additionalAssistanceItems = $stripeAccount->getAdditionalAssistanceItem();
if (!empty($additionalAssistanceItems)) {
foreach ($additionalAssistanceItems as $item) {
$normalized = $this->normalizeAdditionalPriceItem($item, $membership);
if (!empty($normalized)) {
$data['services']['assistance']['additional'][] = $normalized;
}
}
}
$listing = new MemberStripeAccount\Listing();
$listing->setCondition('customerId = :customerId AND source__id = :sourceId', [
'customerId' => $user->getId(),
'sourceId' => $locationSource->getId()
]);
$listing->setLimit(1);
$memberStripeAccount = $listing->current();
if ($memberStripeAccount instanceof MemberStripeAccount) {
// Verifica se ci sono promo code e aggiungi gli sconti ai servizi
if (!empty($memberStripeAccount->getPromoCode())) {
$stripe = new \Stripe\StripeClient($stripeAccount->getStripeSecret());
foreach ($memberStripeAccount->getPromoCode() as $promos) {
$priceId = $promos[0];
$promoId = $promos[1];
// Determina quale servizio corrisponde a questo price_id
$serviceKey = null;
if ($priceId === $stripeAccount->getAttestationItem()) {
$serviceKey = 'attestation';
} elseif ($priceId === $stripeAccount->getAssistanceItem()) {
$serviceKey = 'assistance';
} elseif ($priceId === $stripeAccount->getInsuranceItem()) {
$serviceKey = 'insurance';
}
// if ($serviceKey !== null) {
// // Cerca prima nel database Promocode
// $promo = Promocode::getByPromoid($promoId, 1);
// if ($promo !== null) {
// $coupon = \Pimcore\Model\DataObject\Coupon::getByPromo($promo, 1);
// if ($coupon instanceof \Pimcore\Model\DataObject\Coupon) {
// preg_match('/\d+$/', $coupon->getTitle(), $matches);
// if (!empty($matches)) {
// $data['services'][$serviceKey]['promo'] = [
// 'discount' => (float) $matches[0],
// 'id' => $promoId,
// 'couponName' => $coupon->getTitle(),
// 'promoName' => $promo->getCode(),
// 'code' => $coupon->getCouponid() // Recupera il couponid dalla relazione con promo
// ];
// }
// }
// } else {
// // Se non trovato nel database, recupera da Stripe
// try {
// $promoObj = $stripe->promotionCodes->retrieve($promoId, []);
// if ($promoObj->active) {
// preg_match('/\d+$/', $promoObj->coupon->name, $matches);
// if (!empty($matches)) {
// $data['services'][$serviceKey]['promo'] = [
// 'discount' => (float) $matches[0],
// 'id' => $promoId,
// 'couponName' => '',
// 'promoName' => '',
// 'code' => $promoObj->code // Aggiungi il codice coupon da Stripe
// ];
// }
// }
// } catch (\Exception $e) {
// // Log dell'errore se necessario
// }
// }
// }
}
}
}
$freeToolsData = $this->freeTools($locationSource);
// Merge correttamente i servizi gratuiti con quelli a pagamento
if (isset($freeToolsData['services'])) {
$data['services'] = array_merge($data['services'] ?? [], $freeToolsData['services']);
}
return new Response(json_encode([
'success' => true,
'data' => $data
]));
}
#[Route('/v1/api/stripe/dashboard-stats', name: 'stripe_dashboard_stats', methods: ['GET'])]
public function dashboardStatsAction(Request $request, #[CurrentUser] $user = null): Response
{
// Controlla autenticazione e permessi admin
$authError = $this->validateAdminRole($user);
if ($authError !== null) {
return $authError;
}
$sourceKey = $request->query->get('source');
$period = $request->query->get('period', '30'); // giorni
if (empty($sourceKey)) {
return new Response(json_encode([
'success' => false,
'data' => 'Source parameter required'
]), Response::HTTP_BAD_REQUEST);
}
$locationSource = LocationSource::getByObjectKey($sourceKey, 1);
if (!$locationSource instanceof LocationSource) {
return new Response(json_encode([
'success' => false,
'data' => 'Location source not found'
]), Response::HTTP_NOT_FOUND);
}
$stripeAccount = StripeAccount::getByAccount($locationSource->getId(), 1);
if (!$stripeAccount instanceof StripeAccount) {
return new Response(json_encode([
'success' => false,
'data' => 'Stripe account not found'
]), Response::HTTP_NOT_FOUND);
}
try {
$cacheKey = sprintf('stripe_dashboard_stats_%s_%s', $sourceKey, $period);
$stats = $this->cache->get($cacheKey, function (ItemInterface $item) use ($stripeAccount, $period) {
$item->expiresAfter(300); // 5 minuti di cache
$stripe = new \Stripe\StripeClient($stripeAccount->getStripeSecret());
$startDate = Carbon::now()->subDays((int)$period)->startOfDay();
$endDate = Carbon::now()->endOfDay();
// Recupera i pagamenti nel periodo
$allPayments = $stripe->paymentIntents->all([
'limit' => 100,
'created' => [
'gte' => $startDate->timestamp,
'lte' => $endDate->timestamp
]
]);
// Filtra i pagamenti completati e falliti
$payments = [];
$failedPayments = [];
foreach ($allPayments->data as $payment) {
if ($payment->status === 'succeeded') {
$payments[] = $payment;
} elseif ($payment->status === 'payment_failed') {
$failedPayments[] = $payment;
}
}
return $this->calculateDashboardStats($payments, $failedPayments, $period);
});
return new Response(json_encode([
'success' => true,
'data' => $stats
]));
} catch (\Exception $e) {
$logger = ApplicationLogger::getInstance();
$logger->error('Dashboard stats error: ' . $e->getMessage());
return new Response(json_encode([
'success' => false,
'data' => 'Error retrieving dashboard stats'
]), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/v1/api/stripe/sales-analytics', name: 'stripe_sales_analytics', methods: ['GET'])]
public function salesAnalyticsAction(Request $request, #[CurrentUser] $user = null): Response
{
// Controlla autenticazione e permessi admin
$authError = $this->validateAdminRole($user);
if ($authError !== null) {
return $authError;
}
$sourceKey = $request->query->get('source');
$period = $request->query->get('period', 'month'); // day, week, month
if (empty($sourceKey)) {
return new Response(json_encode([
'success' => false,
'data' => 'Source parameter required'
]), Response::HTTP_BAD_REQUEST);
}
$locationSource = LocationSource::getByObjectKey($sourceKey, 1);
$stripeAccount = StripeAccount::getByAccount($locationSource->getId(), 1);
try {
$cacheKey = sprintf('stripe_sales_analytics_%s_%s', $sourceKey, $period);
$analytics = $this->cache->get($cacheKey, function (ItemInterface $item) use ($stripeAccount, $period) {
// Cache più lungo per analytics storiche
$ttl = match($period) {
'day' => 180, // 3 minuti per dati giornalieri
'week' => 600, // 10 minuti per dati settimanali
'month' => 1800, // 30 minuti per dati mensili
default => 600
};
$item->expiresAfter($ttl);
$stripe = new \Stripe\StripeClient($stripeAccount->getStripeSecret());
return $this->getSalesAnalytics($stripe, $period);
});
return new Response(json_encode([
'success' => true,
'data' => $analytics
]));
} catch (\Exception $e) {
$logger = ApplicationLogger::getInstance();
$logger->error('Sales analytics error: ' . $e->getMessage());
return new Response(json_encode([
'success' => false,
'data' => 'Error retrieving sales analytics'
]), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/v1/api/stripe/alerts', name: 'stripe_alerts', methods: ['GET'])]
public function alertsAction(Request $request, #[CurrentUser] $user = null): Response
{
// Controlla autenticazione e permessi admin
$authError = $this->validateAdminRole($user);
if ($authError !== null) {
return $authError;
}
$sourceKey = $request->query->get('source');
$threshold = (float)$request->query->get('threshold', 20); // percentuale calo ricavi
if (empty($sourceKey)) {
return new Response(json_encode([
'success' => false,
'data' => 'Source parameter required'
]), Response::HTTP_BAD_REQUEST);
}
$locationSource = LocationSource::getByObjectKey($sourceKey, 1);
$stripeAccount = StripeAccount::getByAccount($locationSource->getId(), 1);
try {
$cacheKey = sprintf('stripe_alerts_%s_%.1f', $sourceKey, $threshold);
$alerts = $this->cache->get($cacheKey, function (ItemInterface $item) use ($stripeAccount, $threshold) {
$item->expiresAfter(120); // 2 minuti per gli alert (devono essere abbastanza freschi)
$stripe = new \Stripe\StripeClient($stripeAccount->getStripeSecret());
return $this->generateAlerts($stripe, $threshold);
});
return new Response(json_encode([
'success' => true,
'data' => $alerts
]));
} catch (\Exception $e) {
$logger = ApplicationLogger::getInstance();
$logger->error('Alerts error: ' . $e->getMessage());
return new Response(json_encode([
'success' => false,
'data' => 'Error generating alerts'
]), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/v1/api/stripe/predictions', name: 'stripe_predictions', methods: ['GET'])]
public function predictionsAction(Request $request, #[CurrentUser] $user = null): Response
{
// Controlla autenticazione e permessi admin
$authError = $this->validateAdminRole($user);
if ($authError !== null) {
return $authError;
}
$sourceKey = $request->query->get('source');
if (empty($sourceKey)) {
return new Response(json_encode([
'success' => false,
'data' => 'Source parameter required'
]), Response::HTTP_BAD_REQUEST);
}
$locationSource = LocationSource::getByObjectKey($sourceKey, 1);
$stripeAccount = StripeAccount::getByAccount($locationSource->getId(), 1);
try {
$cacheKey = sprintf('stripe_predictions_%s', $sourceKey);
$predictions = $this->cache->get($cacheKey, function (ItemInterface $item) use ($stripeAccount) {
$item->expiresAfter(900); // 15 minuti per le previsioni
$stripe = new \Stripe\StripeClient($stripeAccount->getStripeSecret());
return $this->calculateMonthlyPrediction($stripe);
});
return new Response(json_encode([
'success' => true,
'data' => $predictions
]));
} catch (\Exception $e) {
$logger = ApplicationLogger::getInstance();
$logger->error('Predictions error: ' . $e->getMessage());
return new Response(json_encode([
'success' => false,
'data' => 'Error calculating predictions'
]), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/v1/api/stripe/clear-cache', name: 'stripe_clear_cache', methods: ['POST'])]
public function clearCacheAction(Request $request, #[CurrentUser] $user = null): Response
{
// Controlla autenticazione e permessi admin
$authError = $this->validateAdminRole($user);
if ($authError !== null) {
return $authError;
}
$sourceKey = $request->request->get('source');
if (empty($sourceKey)) {
return new Response(json_encode([
'success' => false,
'data' => 'Source parameter required'
]), Response::HTTP_BAD_REQUEST);
}
try {
// Pattern per eliminare tutte le cache relative a questa source
$patterns = [
'stripe_dashboard_stats_' . $sourceKey . '_*',
'stripe_sales_analytics_' . $sourceKey . '_*',
'stripe_alerts_' . $sourceKey . '_*',
'stripe_predictions_' . $sourceKey
];
// Pimcore non ha un metodo diretto per eliminare per pattern,
// quindi eliminiamo le chiavi più comuni
$keysToDelete = [
sprintf('stripe_dashboard_stats_%s_7', $sourceKey),
sprintf('stripe_dashboard_stats_%s_30', $sourceKey),
sprintf('stripe_sales_analytics_%s_day', $sourceKey),
sprintf('stripe_sales_analytics_%s_week', $sourceKey),
sprintf('stripe_sales_analytics_%s_month', $sourceKey),
sprintf('stripe_alerts_%s_20.0', $sourceKey),
sprintf('stripe_predictions_%s', $sourceKey)
];
foreach ($keysToDelete as $key) {
$this->cache->delete($key);
}
return new Response(json_encode([
'success' => true,
'data' => 'Cache cleared successfully'
]));
} catch (\Exception $e) {
$logger = ApplicationLogger::getInstance();
$logger->error('Clear cache error: ' . $e->getMessage());
return new Response(json_encode([
'success' => false,
'data' => 'Error clearing cache'
]), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/stripe/transfer', name: 'stripe_transfer', methods: ['POST'])]
public function transferAction(Request $request): Response
{
$username = $request->headers->get('php-auth-user');
$password = $request->headers->get('php-auth-pw');
if (empty($username) && empty($password)) {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
if ($username !== 'ariful' || $password !== 'ariful') {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
$destination = $request->request->get('destination');
$amount = $request->request->get('amount');
// $message = false;
$logger = ApplicationLogger::getInstance();
// $payload = $request->getContent();
// $json = json_decode($payload, true);
try {
Stripe\Stripe::setApiKey($_ENV["STRIPE_SECRET"]);
$stripe = new \Stripe\StripeClient($_ENV["STRIPE_SECRET"]);
$result = $stripe->transfers->create([
'amount' => $amount,
'currency' => 'eur',
'destination' => $destination
]);
} catch (\Exception $e) {
$logger->error($e->getMessage());
}
return new Response($result->toJSON());
}
#[Route('/stripe/balance', name: 'stripe_balance', methods: ['POST'])]
public function balanceAction(Request $request): Response
{
$username = $request->headers->get('php-auth-user');
$password = $request->headers->get('php-auth-pw');
if (empty($username) && empty($password)) {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
if ($username !== 'ariful' || $password !== 'ariful') {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
// $message = false;
$logger = ApplicationLogger::getInstance();
// $payload = $request->getContent();
// $json = json_decode($payload, true);
// dd($json);
try {
Stripe\Stripe::setApiKey($_ENV["STRIPE_SECRET"]);
$stripe = new \Stripe\StripeClient($_ENV["STRIPE_SECRET"]);
$result = $stripe->balance->retrieve([]);
} catch (\Exception $e) {
$logger->error($e->getMessage());
}
return new Response($result->toJSON());
}
private function calculateDashboardStats(array $payments, array $failedPayments, string $period): array
{
$totalRevenue = 0;
$totalTransactions = count($payments);
$dailyRevenue = [];
$lastSuccessfulPayment = null;
$lastFailedPayment = null;
// Calcola ricavi totali e giornalieri
foreach ($payments as $payment) {
$totalRevenue += $payment->amount / 100; // Converti da centesimi
$date = Carbon::createFromTimestamp($payment->created)->format('Y-m-d');
if (!isset($dailyRevenue[$date])) {
$dailyRevenue[$date] = 0;
}
$dailyRevenue[$date] += $payment->amount / 100;
if ($lastSuccessfulPayment === null || $payment->created > $lastSuccessfulPayment['created']) {
$lastSuccessfulPayment = [
'id' => $payment->id,
'amount' => $payment->amount / 100,
'currency' => $payment->currency,
'created' => $payment->created,
'customer' => $payment->customer ?? 'Guest'
];
}
}
// Ultimo pagamento fallito
foreach ($failedPayments as $payment) {
if ($lastFailedPayment === null || $payment->created > $lastFailedPayment['created']) {
$lastFailedPayment = [
'id' => $payment->id,
'amount' => $payment->amount / 100,
'currency' => $payment->currency,
'created' => $payment->created,
'failure_reason' => $payment->last_payment_error->message ?? 'Unknown',
'customer' => $payment->customer ?? 'Guest'
];
}
}
// Trova best e worst day
$bestDay = null;
$worstDay = null;
$bestAmount = 0;
$worstAmount = PHP_FLOAT_MAX;
foreach ($dailyRevenue as $date => $amount) {
if ($amount > $bestAmount) {
$bestAmount = $amount;
$bestDay = ['date' => $date, 'amount' => $amount];
}
if ($amount < $worstAmount) {
$worstAmount = $amount;
$worstDay = ['date' => $date, 'amount' => $amount];
}
}
// Calcola media giornaliera
$avgDailyRevenue = $totalRevenue / max(1, (int)$period);
return [
'totalRevenue' => $totalRevenue,
'totalTransactions' => $totalTransactions,
'avgDailyRevenue' => $avgDailyRevenue,
'failedTransactions' => count($failedPayments),
'dailyRevenue' => $dailyRevenue,
'bestDay' => $bestDay,
'worstDay' => $worstDay,
'lastSuccessfulPayment' => $lastSuccessfulPayment,
'lastFailedPayment' => $lastFailedPayment,
'period' => $period
];
}
private function getSalesAnalytics(\Stripe\StripeClient $stripe, string $period): array
{
$now = Carbon::now();
switch ($period) {
case 'day':
$startDate = $now->copy()->subDays(30)->startOfDay();
$groupBy = 'day';
break;
case 'week':
$startDate = $now->copy()->subWeeks(12)->startOfWeek();
$groupBy = 'week';
break;
case 'month':
default:
$startDate = $now->copy()->subMonths(12)->startOfMonth();
$groupBy = 'month';
break;
}
$allPayments = $stripe->paymentIntents->all([
'limit' => 100,
'created' => [
'gte' => $startDate->timestamp,
'lte' => $now->timestamp
]
]);
// Filtra solo i pagamenti completati
$payments = array_filter($allPayments->data, fn($payment) => $payment->status === 'succeeded');
$analytics = [];
$totals = ['revenue' => 0, 'transactions' => 0];
foreach ($payments as $payment) {
$date = Carbon::createFromTimestamp($payment->created);
$key = match($groupBy) {
'day' => $date->format('Y-m-d'),
'week' => $date->format('Y-W'),
'month' => $date->format('Y-m'),
default => $date->format('Y-m-d')
};
if (!isset($analytics[$key])) {
$analytics[$key] = [
'period' => $key,
'revenue' => 0,
'transactions' => 0,
'avgTransaction' => 0
];
}
$amount = $payment->amount / 100;
$analytics[$key]['revenue'] += $amount;
$analytics[$key]['transactions']++;
$analytics[$key]['avgTransaction'] = $analytics[$key]['revenue'] / $analytics[$key]['transactions'];
$totals['revenue'] += $amount;
$totals['transactions']++;
}
// Ordina per periodo
ksort($analytics);
return [
'period' => $period,
'data' => array_values($analytics),
'totals' => $totals,
'avgTransactionValue' => $totals['transactions'] > 0 ? $totals['revenue'] / $totals['transactions'] : 0
];
}
private function generateAlerts(\Stripe\StripeClient $stripe, float $threshold): array
{
$alerts = [];
$now = Carbon::now();
// Confronta ricavi settimana corrente vs precedente
$thisWeekStart = $now->copy()->startOfWeek();
$lastWeekStart = $now->copy()->subWeek()->startOfWeek();
$lastWeekEnd = $now->copy()->subWeek()->endOfWeek();
$thisWeekAllPayments = $stripe->paymentIntents->all([
'created' => ['gte' => $thisWeekStart->timestamp]
]);
$lastWeekAllPayments = $stripe->paymentIntents->all([
'created' => [
'gte' => $lastWeekStart->timestamp,
'lte' => $lastWeekEnd->timestamp
]
]);
// Filtra i pagamenti per stato
$thisWeekPayments = array_filter($thisWeekAllPayments->data, fn($p) => $p->status === 'succeeded');
$lastWeekPayments = array_filter($lastWeekAllPayments->data, fn($p) => $p->status === 'succeeded');
$thisWeekRevenue = array_sum(array_map(fn($p) => $p->amount / 100, $thisWeekPayments));
$lastWeekRevenue = array_sum(array_map(fn($p) => $p->amount / 100, $lastWeekPayments));
if ($lastWeekRevenue > 0) {
$revenueChange = (($thisWeekRevenue - $lastWeekRevenue) / $lastWeekRevenue) * 100;
if ($revenueChange < -$threshold) {
$alerts[] = [
'type' => 'revenue_drop',
'severity' => 'high',
'message' => sprintf("Calo ricavi del %.1f%% rispetto alla settimana scorsa", $revenueChange),
'data' => [
'thisWeek' => $thisWeekRevenue,
'lastWeek' => $lastWeekRevenue,
'change' => $revenueChange
]
];
}
}
// Alert per pagamenti falliti nelle ultime 24h
$yesterdayStart = $now->copy()->subDay()->startOfDay();
$allRecentPayments = $stripe->paymentIntents->all([
'created' => ['gte' => $yesterdayStart->timestamp]
]);
// Filtra i pagamenti falliti
$failedPayments = array_filter($allRecentPayments->data, fn($p) => $p->status === 'payment_failed');
if (count($failedPayments) > 3) {
$alerts[] = [
'type' => 'failed_payments',
'severity' => 'medium',
'message' => 'Numero elevato di pagamenti falliti nelle ultime 24h (' . count($failedPayments) . ')',
'data' => [
'count' => count($failedPayments),
'period' => '24h'
]
];
}
return $alerts;
}
private function calculateMonthlyPrediction(\Stripe\StripeClient $stripe): array
{
$now = Carbon::now();
$monthStart = $now->copy()->startOfMonth();
$monthEnd = $now->copy()->endOfMonth();
$daysInMonth = $monthEnd->day;
$daysPassed = $now->day;
$daysRemaining = $daysInMonth - $daysPassed;
// Ricavi del mese corrente
$currentMonthAllPayments = $stripe->paymentIntents->all([
'created' => ['gte' => $monthStart->timestamp]
]);
// Filtra solo i pagamenti completati
$currentMonthPayments = array_filter($currentMonthAllPayments->data, fn($p) => $p->status === 'succeeded');
$currentMonthRevenue = array_sum(array_map(fn($p) => $p->amount / 100, $currentMonthPayments));
// Ricavi mese precedente per confronto
$lastMonthStart = $now->copy()->subMonth()->startOfMonth();
$lastMonthEnd = $now->copy()->subMonth()->endOfMonth();
$lastMonthAllPayments = $stripe->paymentIntents->all([
'created' => [
'gte' => $lastMonthStart->timestamp,
'lte' => $lastMonthEnd->timestamp
]
]);
// Filtra solo i pagamenti completati
$lastMonthPayments = array_filter($lastMonthAllPayments->data, fn($p) => $p->status === 'succeeded');
$lastMonthRevenue = array_sum(array_map(fn($p) => $p->amount / 100, $lastMonthPayments));
// Calcola trend giornaliero
$dailyAverage = $daysPassed > 0 ? $currentMonthRevenue / $daysPassed : 0;
$projectedRevenue = $dailyAverage * $daysInMonth;
// Previsione basata su trend degli ultimi 7 giorni
$weekAgoStart = $now->copy()->subDays(7);
$recentAllPayments = $stripe->paymentIntents->all([
'created' => ['gte' => $weekAgoStart->timestamp]
]);
// Filtra solo i pagamenti completati
$recentPayments = array_filter($recentAllPayments->data, fn($p) => $p->status === 'succeeded');
$recentRevenue = array_sum(array_map(fn($p) => $p->amount / 100, $recentPayments));
$recentDailyAvg = $recentRevenue / 7;
$trendBasedProjection = $currentMonthRevenue + ($recentDailyAvg * $daysRemaining);
// Media delle due previsioni
$finalPrediction = ($projectedRevenue + $trendBasedProjection) / 2;
$confidenceLevel = $this->calculateConfidence($daysPassed, $daysInMonth, $currentMonthRevenue, $lastMonthRevenue);
return [
'currentMonthRevenue' => $currentMonthRevenue,
'lastMonthRevenue' => $lastMonthRevenue,
'projectedRevenue' => $finalPrediction,
'dailyAverage' => $dailyAverage,
'recentDailyAverage' => $recentDailyAvg,
'daysInMonth' => $daysInMonth,
'daysPassed' => $daysPassed,
'daysRemaining' => $daysRemaining,
'confidence' => $confidenceLevel,
'changeFromLastMonth' => $lastMonthRevenue > 0 ? (($finalPrediction - $lastMonthRevenue) / $lastMonthRevenue) * 100 : 0
];
}
private function calculateConfidence(int $daysPassed, int $daysInMonth, float $currentRevenue, float $lastMonthRevenue): string
{
$completionPercentage = ($daysPassed / $daysInMonth) * 100;
if ($completionPercentage < 25) {
return 'low';
} elseif ($completionPercentage < 50) {
return 'medium';
} elseif ($completionPercentage < 75) {
return 'high';
} else {
return 'very_high';
}
}
private function fetchEmailFromOrder($url, $queryParams = []): string|null
{
$logger = ApplicationLogger::getInstance();
try {
// Inizializza cURL
$ch = curl_init();
// Configura le opzioni cURL
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Per ottenere il risultato come stringa
curl_setopt($ch, CURLOPT_HTTPGET, true); // Metodo GET
// Aggiungi query string (facoltativo)
curl_setopt($ch, CURLOPT_URL, $url . '/api/getOrderUser?' . http_build_query($queryParams));
// Esegui la richiesta
$response = curl_exec($ch);
// dd($url . '/api/getOrderUser?' . http_build_query($queryParams), $response);
// Controlla eventuali errori
if ($response === false) {
$error = curl_error($ch);
// dd($error);
curl_close($ch);
return null;
}
// Recupera il codice di stato HTTP
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Chiudi cURL
curl_close($ch);
// Decodifica JSON (se applicabile)
$data = json_decode($response, true);
if (isset($data['data'])) {
return $data['data'];
} else {
return null;
}
} catch(\Exception $e) {
$logger->error($e);
}
return null;
}
#[Route('/stripe/webhook', name: 'app_stripe_webhook', methods: ['POST'])]
public function verify(Request $request): Response
{
$message = "";
$logger = ApplicationLogger::getInstance();
$payload = $request->getContent();
$json = json_decode($payload, true);
$locationSource = null;
$member = null;
$userStripeAccount = null;
$logger->info(serialize($json['data']['object']));
try {
if (empty($json['data']['object']['customer'])) {
if (isset($json['data']['object']['success_url']) && str_contains($json['data']['object']['success_url'], "https://servizi.confabitaremonzabrianza.com")) {
$locationSource = LocationSource::getByPath('/location-source/monzabrianza');
} elseif(isset($json['data']['object']['success_url']) && str_contains($json['data']['object']['success_url'], "https://servizi.bergamoconfabitare.com")) {
$locationSource = LocationSource::getByPath('/location-source/bergamo');
} elseif(isset($json['data']['object']['metadata']['source'])) {
$source = $json['data']['object']['metadata']['source'];
$locationSource = LocationSource::getByPath("/location-source/$source");
$logger->info($source);
}
if ($locationSource instanceof LocationSource && isset($json['data']['object']['metadata']['order_id'])) {
if ( isset($json['data']['object']['recovered_from']) && $json['data']['object']['recovered_from'] !== null) {
$response = $this->fetchEmailFromOrder($locationSource->getServiceEndpoint()?->getHref(), [
'checkout_session' => $json['data']['object']['recovered_from'],
'order_id' => $json['data']['object']['metadata']['order_id']
]);
} elseif(isset($json['data']['object']['id']) && $json['data']['object']['id'] !== null) {
$logger->info($locationSource->getServiceEndpoint()?->getHref());
$response = $this->fetchEmailFromOrder($locationSource->getServiceEndpoint()?->getHref(), [
'checkout_session' => $json['data']['object']['id'],
'order_id' => $json['data']['object']['metadata']['order_id']
]);
}
if (!empty($response)) {
$members = new MembersUser\Listing();
$members->setCondition('email = :email', [
'email' => $response
]);
$members->setUnpublished(true);
$members->setLimit(1);
$member = $members->current();
// $member = MembersUser::getByEmail($response, 1);
if ($member instanceof MembersUser) {
$MemberStripeAccountListing = new MemberStripeAccount\Listing();
$MemberStripeAccountListing->setUnpublished(false);
$MemberStripeAccountListing->setCondition('customerId = :customerId AND source__id = :source', [
'customerId' => $member->getId(),
'source' => $locationSource->getId()
]);
$MemberStripeAccountListing->setLimit(1);
$userStripeAccount = $MemberStripeAccountListing->current();
} else {
$logger->error("Member not found from order: ". $json['id']);
}
} else {
$logger->info($json['data']['object']['customer_email']);
$members = new MembersUser\Listing();
$members->setCondition('email = :email', [
'email' => $json['data']['object']['customer_email']
]);
$members->setUnpublished(true);
$members->setLimit(1);
$member = $members->current();
// $member = MembersUser::getByEmail($json['data']['object']['customer_email'], 1);
if ($member instanceof MembersUser) {
$MemberStripeAccountListing = new MemberStripeAccount\Listing();
$MemberStripeAccountListing->setUnpublished(false);
$MemberStripeAccountListing->setCondition('customerId = :customerId AND source__id = :source', [
'customerId' => $member->getId(),
'source' => $locationSource->getId()
]);
$MemberStripeAccountListing->setLimit(1);
$userStripeAccount = $MemberStripeAccountListing->current();
} else {
$logger->error("Member not found: ". $json['id']);
}
}
} else {
$logger->error("Source or order_id not found: ". $json['id']);
}
} else {
$userStripeAccount = MemberStripeAccount::getByStripeCustomerId($json['data']['object']['customer'], 1);
}
} catch(\Exception $e) {
$logger->error($e);
}
if ($userStripeAccount == null || !$userStripeAccount instanceof MemberStripeAccount) {
$logger->error("STRIPE ACCOUNT NOT FOUND");
// Invalid payload
echo '⚠️ STRIPE ACCOUNT NOT FOUND.';
http_response_code(200);
exit();
}
$stripeAccount = $userStripeAccount->getSource()->getStripeAccount();
$members = new MembersUser\Listing();
$members->setCondition('oo_id = :id', [
'id' => $userStripeAccount->getCustomerId()
]);
$members->setUnpublished(true);
$members->setLimit(1);
$member = $members->current();
// $member = MembersUser::getById($userStripeAccount->getCustomerId());
if (!$member instanceof MembersUser) {
$logger->error("MEMBER ACCOUNT NOT FOUND");
// Invalid payload
echo '⚠️ MEMBER ACCOUNT NOT FOUND.';
http_response_code(200);
exit();
}
Stripe\Stripe::setApiKey($stripeAccount->getStripeSecret());
$stripe = new \Stripe\StripeClient($stripeAccount->getStripeSecret());
// Replace this endpoint secret with your endpoint's unique secret
// If you are testing with the CLI, find the secret by running 'stripe listen'
// If you are using an endpoint defined with the API or dashboard, look in your webhook settings
// at https://dashboard.stripe.com/webhooks
$endpoint_secret = $stripeAccount->getStripeWebhookSecret();
$event = null;
try {
$event = Stripe\Event::constructFrom($json);
} catch (\UnexpectedValueException $e) {
$logger->error($e);
// Invalid payload
echo '⚠️ Webhook error while parsing basic request.';
http_response_code(400);
exit();
}
if ($endpoint_secret) {
// Only verify the event if there is an endpoint secret defined
// Otherwise use the basic decoded event
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$sig_header,
$endpoint_secret
);
} catch (\Stripe\Exception\SignatureVerificationException $e) {
$logger->error($e);
// Invalid signature
echo '⚠️ Webhook error while validating signature.';
http_response_code(400);
exit();
}
}
$subscriptEventArray = [
'customer.subscription.created',
'customer.subscription.deleted',
'customer.subscription.updated',
'checkout.session.completed',
'checkout.session.async_payment_succeeded'
];
$logger->info($event->type);
if (in_array($event->type, $subscriptEventArray)) {
// $member = MembersUser::getByStripeCustomerId($event->data->object['customer'], ['limit' => 1, 'unpublished' => true]);
if ($member !== null) {
switch ($event->type) {
case 'checkout.session.completed':
case 'checkout.session.async_payment_succeeded':
if ($event->data->object['status'] === 'complete' && $event->data->object['payment_status'] === 'paid') {
$subs = $event->data->object;
try {
// $logger->info(count($member->getPromocode()));
// $logger->info($member->getGroups()[0]->getName());
if (count($userStripeAccount->getPromocode()) == 0 && isset($member->getGroups()[0]) && $member->getGroups()[0]->getName() === 'Private') {
$subscriptionEnd = Carbon::now()->addYear();
$promoCodeAssistenza = $stripe->promotionCodes->create([
'coupon' => $stripeAccount->getPrivateAssitancePromo(),
'customer' => $userStripeAccount->getStripeCustomerId(),
'expires_at' => $subscriptionEnd->timestamp
]);
$promoCodeAttestazione = $stripe->promotionCodes->create([
'coupon' => $stripeAccount->getPrivateAttestationPromo(),
'customer' => $userStripeAccount->getStripeCustomerId(),
'expires_at' => $subscriptionEnd->timestamp
]);
// $userStripeAccount->setPromocode([]);
// $userStripeAccount->save();
$promos = [];
$promos[] = [$stripeAccount->getAssistanceItem(), $promoCodeAssistenza->id];
$promos[] = [$stripeAccount->getAttestationItem(), $promoCodeAttestazione->id];
if ($promos) {
$member->setEndDate($subscriptionEnd);
$userStripeAccount->setPromocode($promos);
$userStripeAccount->save();
$member->save();
}
} else {
$logger->warning("Promo not applicable. Business user for event: ". $json['id']);
}
$logger->info($event->type . ' ->SUCCESS');
} catch (\Exception $e) {
$logger->error($e->getMessage());
$message = $e->getMessage();
}
} else {
$logger->error("Error status: ". $json['id']);
}
break;
case 'customer.subscription.created':
$subs = $event->data->object;
if ($subs instanceof \Stripe\Subscription) {
// $member->setCreatedDate(Carbon::createFromTimestamp($subs->created));
// $message = $member->save();
}
break;
case 'customer.subscription.deleted':
$subs = $event->data->object;
if ($subs instanceof \Stripe\Subscription) {
// $member->setEndDate(Carbon::createFromTimestamp($subs->ended_at));
// $member->setActive(false);
// $message = $member->save();
}
break;
case 'customer.subscription.updated':
$subs = $event->data->object;
if ($subs instanceof \Stripe\Subscription) {
// $member->setLastUpdateDate(Carbon::createFromTimestamp(time()));
// if ($subs->status === 'paid' || $subs->status === 'active') {
// $member->setActive(true);
// } else {
// $member->setActive(false);
// }
//
// $message = $member->save();
}
break;
default:
// Unexpected event type
$message = 'Received unknown event type';
}
} else {
$logger->error("Member not found: ". $json['id']);
}
} else {
$logger->error("Event not found: ". $json['id']);
}
if ($message !== "") {
$logger->error($message);
// Invalid payload
echo $message;
} else {
echo '✅ Success!';
}
http_response_code(200);
exit();
}
// #[Route('/stripe', name: 'app_stripe')]
// public function index(): Response
// {
// Stripe\Stripe::setApiKey($_ENV["STRIPE_SECRET"]);
// $checkout_session = Stripe\Checkout\Session::create([
// 'customer_email' => "work@mdariful.com",
// 'line_items' => [[
// # Provide the exact Price ID (e.g. pr_1234) of the product you want to sell
// 'price' => "price_1Mf1fSLneZNUF8luklGXV2KU",
// 'quantity' => 1,
// ]],
// 'allow_promotion_codes' => true,
// 'mode' => 'subscription',
// 'success_url' => "http://localhost:8009/stripe/success?session_id={CHECKOUT_SESSION_ID}",
// 'cancel_url' => "http://localhost:8009/stripe/cancel?session_id={CHECKOUT_SESSION_ID}",
// 'tax_id_collection' => [
// 'enabled' => true,
// ],
// 'billing_address_collection'=> 'required'
// ]);
//
// return $this->redirect($checkout_session->url);
// }
//
// #[Route('/stripe/success', name: 'app_stripe_success')]
// public function success(Request $request): Response
// {
// Stripe\Stripe::setApiKey($_ENV["STRIPE_SECRET"]);
// $session_id = $request->query->get('session_id');
// $return_url = "http://localhost:8009";
//
// $checkout_session = Stripe\Checkout\Session::retrieve($session_id);
// // Authenticate your user.
// $session = Stripe\BillingPortal\Session::create([
// 'customer' => $checkout_session->customer,
// 'return_url' => $return_url,
// ]);
//
// return $this->redirect($session->url);
// }
//
// #[Route('/stripe/cancel', name: 'app_stripe_cancel')]
// public function cancel(): Response
// {
// return false;
// }
}