Initial commit: Slim Framework 4 API Retribusi dengan modular architecture
This commit is contained in:
157
src/Modules/Retribusi/Summary/DailySummaryService.php
Normal file
157
src/Modules/Retribusi/Summary/DailySummaryService.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Retribusi\Summary;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
class DailySummaryService
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct(PDO $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate daily summary for a specific date
|
||||
*
|
||||
* @param string $date Format: Y-m-d
|
||||
* @return array ['rows_processed' => int, 'date' => string]
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function aggregateForDate(string $date): array
|
||||
{
|
||||
// Validate date format
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
|
||||
throw new \InvalidArgumentException('Invalid date format. Expected Y-m-d');
|
||||
}
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
try {
|
||||
// Aggregate from entry_events
|
||||
// Only count events from active locations, gates, and tariffs
|
||||
$sql = "
|
||||
SELECT
|
||||
DATE(e.event_time) as summary_date,
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(t.amount, 0) as tariff_amount
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
GROUP BY
|
||||
DATE(e.event_time),
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
t.amount
|
||||
";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([$date]);
|
||||
$aggregated = $stmt->fetchAll();
|
||||
|
||||
$rowsProcessed = 0;
|
||||
|
||||
// Upsert to daily_summary
|
||||
$upsertSql = "
|
||||
INSERT INTO daily_summary
|
||||
(summary_date, location_code, gate_code, category, total_count, total_amount, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_count = VALUES(total_count),
|
||||
total_amount = VALUES(total_amount),
|
||||
updated_at = NOW()
|
||||
";
|
||||
|
||||
$upsertStmt = $this->db->prepare($upsertSql);
|
||||
|
||||
foreach ($aggregated as $row) {
|
||||
$totalAmount = (int) $row['total_count'] * (int) $row['tariff_amount'];
|
||||
|
||||
$upsertStmt->execute([
|
||||
$row['summary_date'],
|
||||
$row['location_code'],
|
||||
$row['gate_code'],
|
||||
$row['category'],
|
||||
(int) $row['total_count'],
|
||||
$totalAmount
|
||||
]);
|
||||
|
||||
$rowsProcessed++;
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
return [
|
||||
'rows_processed' => $rowsProcessed,
|
||||
'date' => $date
|
||||
];
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$this->db->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get daily summary data
|
||||
*
|
||||
* @param string $date
|
||||
* @param string|null $locationCode
|
||||
* @param string|null $gateCode
|
||||
* @return array
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function getDailySummary(
|
||||
string $date,
|
||||
?string $locationCode = null,
|
||||
?string $gateCode = null
|
||||
): array {
|
||||
$sql = "
|
||||
SELECT
|
||||
summary_date,
|
||||
location_code,
|
||||
gate_code,
|
||||
category,
|
||||
total_count,
|
||||
total_amount
|
||||
FROM daily_summary
|
||||
WHERE summary_date = ?
|
||||
";
|
||||
|
||||
$params = [$date];
|
||||
|
||||
if ($locationCode !== null) {
|
||||
$sql .= " AND location_code = ?";
|
||||
$params[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$sql .= " AND gate_code = ?";
|
||||
$params[] = $gateCode;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY location_code, gate_code, category";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
|
||||
195
src/Modules/Retribusi/Summary/HourlySummaryService.php
Normal file
195
src/Modules/Retribusi/Summary/HourlySummaryService.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Retribusi\Summary;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
class HourlySummaryService
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct(PDO $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate hourly summary for a specific date
|
||||
*
|
||||
* @param string $date Format: Y-m-d
|
||||
* @return array ['rows_processed' => int, 'date' => string]
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function aggregateForDate(string $date): array
|
||||
{
|
||||
// Validate date format
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
|
||||
throw new \InvalidArgumentException('Invalid date format. Expected Y-m-d');
|
||||
}
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
try {
|
||||
// Aggregate from entry_events
|
||||
// Group by hour, location, gate, category
|
||||
// Only count events from active locations, gates, and tariffs
|
||||
$sql = "
|
||||
SELECT
|
||||
DATE(e.event_time) as summary_date,
|
||||
HOUR(e.event_time) as summary_hour,
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(t.amount, 0) as tariff_amount
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
AND t.is_active = 1
|
||||
WHERE DATE(e.event_time) = ?
|
||||
GROUP BY
|
||||
DATE(e.event_time),
|
||||
HOUR(e.event_time),
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
t.amount
|
||||
";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([$date]);
|
||||
$aggregated = $stmt->fetchAll();
|
||||
|
||||
$rowsProcessed = 0;
|
||||
|
||||
// Upsert to hourly_summary
|
||||
$upsertSql = "
|
||||
INSERT INTO hourly_summary
|
||||
(summary_date, summary_hour, location_code, gate_code, category, total_count, total_amount, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_count = VALUES(total_count),
|
||||
total_amount = VALUES(total_amount),
|
||||
updated_at = NOW()
|
||||
";
|
||||
|
||||
$upsertStmt = $this->db->prepare($upsertSql);
|
||||
|
||||
foreach ($aggregated as $row) {
|
||||
$totalAmount = (int) $row['total_count'] * (int) $row['tariff_amount'];
|
||||
|
||||
$upsertStmt->execute([
|
||||
$row['summary_date'],
|
||||
(int) $row['summary_hour'],
|
||||
$row['location_code'],
|
||||
$row['gate_code'],
|
||||
$row['category'],
|
||||
(int) $row['total_count'],
|
||||
$totalAmount
|
||||
]);
|
||||
|
||||
$rowsProcessed++;
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
return [
|
||||
'rows_processed' => $rowsProcessed,
|
||||
'date' => $date
|
||||
];
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$this->db->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hourly summary data for chart
|
||||
*
|
||||
* @param string $date
|
||||
* @param string|null $locationCode
|
||||
* @param string|null $gateCode
|
||||
* @return array
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function getHourlySummary(
|
||||
string $date,
|
||||
?string $locationCode = null,
|
||||
?string $gateCode = null
|
||||
): array {
|
||||
$sql = "
|
||||
SELECT
|
||||
summary_hour,
|
||||
SUM(total_count) as total_count,
|
||||
SUM(total_amount) as total_amount
|
||||
FROM hourly_summary
|
||||
WHERE summary_date = ?
|
||||
";
|
||||
|
||||
$params = [$date];
|
||||
|
||||
if ($locationCode !== null) {
|
||||
$sql .= " AND location_code = ?";
|
||||
$params[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$sql .= " AND gate_code = ?";
|
||||
$params[] = $gateCode;
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY summary_hour ORDER BY summary_hour ASC";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$results = $stmt->fetchAll();
|
||||
|
||||
// Initialize arrays for all 24 hours (0-23)
|
||||
$hourlyData = [];
|
||||
for ($hour = 0; $hour < 24; $hour++) {
|
||||
$hourlyData[$hour] = [
|
||||
'total_count' => 0,
|
||||
'total_amount' => 0
|
||||
];
|
||||
}
|
||||
|
||||
// Fill in actual data
|
||||
foreach ($results as $row) {
|
||||
$hour = (int) $row['summary_hour'];
|
||||
$hourlyData[$hour] = [
|
||||
'total_count' => (int) $row['total_count'],
|
||||
'total_amount' => (int) $row['total_amount']
|
||||
];
|
||||
}
|
||||
|
||||
// Build labels and series
|
||||
$labels = [];
|
||||
$totalCounts = [];
|
||||
$totalAmounts = [];
|
||||
|
||||
for ($hour = 0; $hour < 24; $hour++) {
|
||||
$labels[] = str_pad((string) $hour, 2, '0', STR_PAD_LEFT);
|
||||
$totalCounts[] = $hourlyData[$hour]['total_count'];
|
||||
$totalAmounts[] = $hourlyData[$hour]['total_amount'];
|
||||
}
|
||||
|
||||
return [
|
||||
'labels' => $labels,
|
||||
'series' => [
|
||||
'total_count' => $totalCounts,
|
||||
'total_amount' => $totalAmounts
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
253
src/Modules/Retribusi/Summary/SummaryController.php
Normal file
253
src/Modules/Retribusi/Summary/SummaryController.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Retribusi\Summary;
|
||||
|
||||
use App\Support\ResponseHelper;
|
||||
use InvalidArgumentException;
|
||||
use PDOException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class SummaryController
|
||||
{
|
||||
private DailySummaryService $dailyService;
|
||||
private HourlySummaryService $hourlyService;
|
||||
|
||||
public function __construct(
|
||||
DailySummaryService $dailyService,
|
||||
HourlySummaryService $hourlyService
|
||||
) {
|
||||
$this->dailyService = $dailyService;
|
||||
$this->hourlyService = $hourlyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger daily summary aggregation (admin only)
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function aggregateDaily(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
): ResponseInterface {
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
if (!is_array($body)) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['body' => 'Invalid JSON body']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
$date = $body['date'] ?? null;
|
||||
|
||||
if ($date === null || !is_string($date)) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Field is required and must be a string (Y-m-d format)']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Invalid date format. Expected Y-m-d (e.g., 2025-01-01)']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->dailyService->aggregateForDate($date);
|
||||
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'timestamp' => time()
|
||||
]
|
||||
);
|
||||
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'message' => $e->getMessage()
|
||||
],
|
||||
422
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'server_error',
|
||||
'message' => 'Database error occurred'
|
||||
],
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get daily summary data (viewer/operator/admin)
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getDailySummary(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
): ResponseInterface {
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$date = $queryParams['date'] ?? null;
|
||||
if ($date === null || !is_string($date)) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Query parameter date is required (Y-m-d format)']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Invalid date format. Expected Y-m-d (e.g., 2025-01-01)']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
$locationCode = $queryParams['location_code'] ?? null;
|
||||
if ($locationCode !== null && !is_string($locationCode)) {
|
||||
$locationCode = null;
|
||||
}
|
||||
|
||||
$gateCode = $queryParams['gate_code'] ?? null;
|
||||
if ($gateCode !== null && !is_string($gateCode)) {
|
||||
$gateCode = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->dailyService->getDailySummary($date, $locationCode, $gateCode);
|
||||
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'timestamp' => time()
|
||||
]
|
||||
);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'server_error',
|
||||
'message' => 'Database error occurred'
|
||||
],
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hourly summary data for chart (viewer/operator/admin)
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getHourlySummary(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
): ResponseInterface {
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$date = $queryParams['date'] ?? null;
|
||||
if ($date === null || !is_string($date)) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Query parameter date is required (Y-m-d format)']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Invalid date format. Expected Y-m-d (e.g., 2025-01-01)']
|
||||
],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
$locationCode = $queryParams['location_code'] ?? null;
|
||||
if ($locationCode !== null && !is_string($locationCode)) {
|
||||
$locationCode = null;
|
||||
}
|
||||
|
||||
$gateCode = $queryParams['gate_code'] ?? null;
|
||||
if ($gateCode !== null && !is_string($gateCode)) {
|
||||
$gateCode = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->hourlyService->getHourlySummary($date, $locationCode, $gateCode);
|
||||
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'timestamp' => time()
|
||||
]
|
||||
);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'server_error',
|
||||
'message' => 'Database error occurred'
|
||||
],
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
src/Modules/Retribusi/Summary/SummaryRoutes.php
Normal file
68
src/Modules/Retribusi/Summary/SummaryRoutes.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Retribusi\Summary;
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Middleware\JwtMiddleware;
|
||||
use App\Middleware\RoleMiddleware;
|
||||
use App\Support\Database;
|
||||
use Slim\App;
|
||||
|
||||
class SummaryRoutes
|
||||
{
|
||||
/**
|
||||
* Register summary routes
|
||||
*
|
||||
* @param App $app
|
||||
* @return void
|
||||
*/
|
||||
public static function register(App $app): void
|
||||
{
|
||||
// JWT middleware
|
||||
$jwtMiddleware = new JwtMiddleware();
|
||||
|
||||
// Admin role middleware
|
||||
$adminRoleMiddleware = new RoleMiddleware(['admin']);
|
||||
|
||||
// Get database connection
|
||||
$dbHost = AppConfig::get('DB_HOST', 'localhost');
|
||||
$dbName = AppConfig::get('DB_NAME', '');
|
||||
$dbUser = AppConfig::get('DB_USER', '');
|
||||
$dbPass = AppConfig::get('DB_PASS', '');
|
||||
|
||||
$db = Database::getConnection($dbHost, $dbName, $dbUser, $dbPass);
|
||||
|
||||
// Initialize services and controller
|
||||
$dailySummaryService = new DailySummaryService($db);
|
||||
$hourlySummaryService = new HourlySummaryService($db);
|
||||
$summaryController = new SummaryController($dailySummaryService, $hourlySummaryService);
|
||||
|
||||
// Register routes
|
||||
$app->group('/retribusi', function ($group) use (
|
||||
$jwtMiddleware,
|
||||
$adminRoleMiddleware,
|
||||
$summaryController
|
||||
) {
|
||||
$group->group('/v1', function ($v1Group) use (
|
||||
$jwtMiddleware,
|
||||
$adminRoleMiddleware,
|
||||
$summaryController
|
||||
) {
|
||||
// Admin endpoint: trigger aggregation
|
||||
$v1Group->post('/admin/summary/daily', [$summaryController, 'aggregateDaily'])
|
||||
->add($adminRoleMiddleware)
|
||||
->add($jwtMiddleware);
|
||||
|
||||
// Read endpoints: get summaries
|
||||
$v1Group->get('/summary/daily', [$summaryController, 'getDailySummary'])
|
||||
->add($jwtMiddleware);
|
||||
|
||||
$v1Group->get('/summary/hourly', [$summaryController, 'getHourlySummary'])
|
||||
->add($jwtMiddleware);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user