Skip to content
Open
175 changes: 175 additions & 0 deletions api/subscriptions/get_period_budget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php
/*
This API Endpoint accepts both POST and GET requests.
It receives:
- api_key (or apiKey): the API key of the user.
- reference_date (optional): date in YYYY-MM-DD format to evaluate the active budget period (defaults to today).

It returns:
- success: whether the request was successful (boolean).
- title: endpoint title (string).
- period_budget: configured period budget amount (float).
- amount_needed_this_period: projected required amount from reference_date to period_end (float).
- amount_needed_full_period: projected required amount from period_start to period_end (float).
- amount_remaining_this_period: remaining amount before hitting budget (float).
- amount_over_budget: amount above budget, if any (float).
- is_over_budget: whether projected spend exceeds period budget (boolean).
- budget_period_type: weekly, fortnightly, monthly.
- budget_period_anchor_date: anchor date in YYYY-MM-DD.
- period_start: active period start date in YYYY-MM-DD.
- period_end: active period end date in YYYY-MM-DD.
- period_label: human-readable active period label.
- currency_code: main currency code.
- currency_symbol: main currency symbol.
- reference_date: evaluated date in YYYY-MM-DD.
- notes: warnings or additional information (array).
*/

require_once '../../includes/connect_endpoint.php';
require_once '../../includes/budget_period_calculations.php';

header('Content-Type: application/json; charset=UTF-8');

if ($_SERVER["REQUEST_METHOD"] !== "POST" && $_SERVER["REQUEST_METHOD"] !== "GET") {
echo json_encode([
"success" => false,
"title" => "Invalid request method"
]);
exit;
}

$rawBody = file_get_contents('php://input');
$jsonData = json_decode($rawBody, true);
$payload = is_array($jsonData) ? $jsonData : [];

$apiKey = $_REQUEST['api_key']
?? $_REQUEST['apiKey']
?? $payload['api_key']
?? $payload['apiKey']
?? null;
if (!$apiKey) {
echo json_encode([
"success" => false,
"title" => "Missing parameters"
]);
exit;
}

$referenceDateRaw = $_REQUEST['reference_date']
?? $payload['reference_date']
?? null;
if ($referenceDateRaw !== null && $referenceDateRaw !== '') {
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $referenceDateRaw)) {
echo json_encode([
"success" => false,
"title" => "Invalid parameter",
"notes" => ["reference_date must use YYYY-MM-DD format."]
]);
exit;
}

$referenceDate = DateTime::createFromFormat('Y-m-d', $referenceDateRaw);
if ($referenceDate === false || $referenceDate->format('Y-m-d') !== $referenceDateRaw) {
echo json_encode([
"success" => false,
"title" => "Invalid parameter",
"notes" => ["reference_date must be a valid calendar date."]
]);
exit;
}
} else {
$referenceDate = new DateTime('now');
}

$sql = "SELECT id, main_currency, period_budget, budget_period_type, budget_period_anchor_date FROM user WHERE api_key = :apiKey";
$stmt = $db->prepare($sql);
$stmt->bindValue(':apiKey', $apiKey, SQLITE3_TEXT);
$result = $stmt->execute();
$user = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;

if (!$user) {
echo json_encode([
"success" => false,
"title" => "Invalid API key"
]);
exit;
}

$userId = (int) $user['id'];
$periodBudget = max(0, (float) ($user['period_budget'] ?? 0));
$periodType = sanitizeBudgetPeriodType($user['budget_period_type'] ?? 'monthly');
$anchorDate = sanitizeBudgetAnchorDate($user['budget_period_anchor_date'] ?? getDefaultBudgetAnchorDate());

$activePeriod = getActiveBudgetPeriod($referenceDate, $periodType, $anchorDate);

$subsSql = "SELECT * FROM subscriptions WHERE user_id = :userId AND inactive = 0";
$subsStmt = $db->prepare($subsSql);
$subsStmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$subsResult = $subsStmt->execute();
$subscriptions = [];
while ($subsResult && ($subscription = $subsResult->fetchArray(SQLITE3_ASSOC))) {
$subscriptions[] = $subscription;
}

$amountNeededFromReference = computeAmountNeededInPeriod(
$subscriptions,
$referenceDate,
$activePeriod['end'],
$db,
$userId
);

$amountNeededFullPeriod = computeAmountNeededInPeriod(
$subscriptions,
$activePeriod['start'],
$activePeriod['end'],
$db,
$userId
);

$amountRemaining = max(0, $periodBudget - $amountNeededFromReference);
$amountOverBudget = max(0, $amountNeededFromReference - $periodBudget);
$isOverBudget = $periodBudget > 0 && $amountNeededFromReference > $periodBudget;

$currencyCode = null;
$currencySymbol = null;
$currencySql = "SELECT code, symbol FROM currencies WHERE id = :currencyId AND user_id = :userId LIMIT 1";
$currencyStmt = $db->prepare($currencySql);
$currencyStmt->bindValue(':currencyId', (int) $user['main_currency'], SQLITE3_INTEGER);
$currencyStmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$currencyResult = $currencyStmt->execute();
$currency = $currencyResult ? $currencyResult->fetchArray(SQLITE3_ASSOC) : false;

if ($currency) {
$currencyCode = $currency['code'];
$currencySymbol = $currency['symbol'];
}

$notes = [];
if ($periodBudget <= 0) {
$notes[] = "Period budget is set to 0.";
}

echo json_encode([
"success" => true,
"title" => "period_budget",
"period_budget" => round($periodBudget, 2),
"amount_needed_this_period" => round($amountNeededFromReference, 2),
"amount_needed_full_period" => round($amountNeededFullPeriod, 2),
"amount_remaining_this_period" => round($amountRemaining, 2),
"amount_over_budget" => round($amountOverBudget, 2),
"is_over_budget" => $isOverBudget,
"budget_period_type" => $periodType,
"budget_period_anchor_date" => $anchorDate,
"period_start" => $activePeriod['start']->format('Y-m-d'),
"period_end" => $activePeriod['end']->format('Y-m-d'),
"period_label" => $activePeriod['label'],
"currency_code" => $currencyCode,
"currency_symbol" => $currencySymbol,
"reference_date" => $referenceDate->format('Y-m-d'),
"notes" => $notes
], JSON_UNESCAPED_UNICODE);

$db->close();

?>
142 changes: 142 additions & 0 deletions api/users/set_budget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
/*
This API Endpoint accepts POST requests only.
It receives:
- api_key (or apiKey): the API key of the user.
- monthly_budget or budget (optional): monthly budget to store in user.budget.
- period_budget (optional): period budget to store in user.period_budget.
- budget_period_type (optional): weekly, fortnightly, monthly.
- budget_period_anchor_date (optional): YYYY-MM-DD.

At least one budget-related field is required.
*/

require_once '../../includes/connect_endpoint.php';
require_once '../../includes/budget_period_calculations.php';

header('Content-Type: application/json; charset=UTF-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode([
'success' => false,
'title' => 'Invalid request method',
'message' => 'Only POST requests are allowed.'
]);
exit;
}

$rawBody = file_get_contents('php://input');
$jsonData = json_decode($rawBody, true);
$payload = is_array($jsonData) ? $jsonData : $_POST;

$apiKey = $payload['api_key'] ?? $payload['apiKey'] ?? $_POST['api_key'] ?? $_POST['apiKey'] ?? null;

if (!$apiKey) {
echo json_encode([
'success' => false,
'title' => 'Missing API key',
'message' => 'API key is required.'
]);
exit;
}

$sql = "SELECT id FROM user WHERE api_key = :apiKey";
$stmt = $db->prepare($sql);
$stmt->bindValue(':apiKey', $apiKey, SQLITE3_TEXT);
$result = $stmt->execute();
$user = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;

if (!$user) {
echo json_encode([
'success' => false,
'title' => 'Unauthorized',
'message' => 'Invalid API key.'
]);
exit;
}

$hasMonthlyBudget = array_key_exists('monthly_budget', $payload) || array_key_exists('budget', $payload);
$hasPeriodBudget = array_key_exists('period_budget', $payload);
$hasPeriodMeta = array_key_exists('budget_period_type', $payload) || array_key_exists('budget_period_anchor_date', $payload);

if (!$hasMonthlyBudget && !$hasPeriodBudget && !$hasPeriodMeta) {
echo json_encode([
'success' => false,
'title' => 'Missing parameters',
'message' => 'Provide at least one of monthly_budget/budget/period_budget/budget_period_type/budget_period_anchor_date.'
]);
exit;
}

$sets = [];
$binds = [];

if ($hasMonthlyBudget) {
$monthlyBudgetRaw = $payload['monthly_budget'] ?? $payload['budget'];
if (!is_numeric($monthlyBudgetRaw)) {
echo json_encode([
'success' => false,
'title' => 'Invalid parameter',
'message' => 'monthly_budget (or budget) must be numeric.'
]);
exit;
}

$monthlyBudget = max(0, (float) $monthlyBudgetRaw);
$sets[] = 'budget = :monthlyBudget';
$binds[':monthlyBudget'] = ['value' => $monthlyBudget, 'type' => SQLITE3_FLOAT];
}

if ($hasPeriodBudget || $hasPeriodMeta) {
if ($hasPeriodBudget) {
$periodBudgetRaw = $payload['period_budget'];
if (!is_numeric($periodBudgetRaw)) {
echo json_encode([
'success' => false,
'title' => 'Invalid parameter',
'message' => 'period_budget must be numeric.'
]);
exit;
}

$periodBudget = max(0, (float) $periodBudgetRaw);
$sets[] = 'period_budget = :periodBudget';
$binds[':periodBudget'] = ['value' => $periodBudget, 'type' => SQLITE3_FLOAT];
}

$periodType = sanitizeBudgetPeriodType($payload['budget_period_type'] ?? 'monthly');
$anchorDate = sanitizeBudgetAnchorDate($payload['budget_period_anchor_date'] ?? getDefaultBudgetAnchorDate());

$sets[] = 'budget_period_type = :periodType';
$binds[':periodType'] = ['value' => $periodType, 'type' => SQLITE3_TEXT];
$sets[] = 'budget_period_anchor_date = :anchorDate';
$binds[':anchorDate'] = ['value' => $anchorDate, 'type' => SQLITE3_TEXT];
}

$updateSql = "UPDATE user SET " . implode(', ', $sets) . " WHERE id = :userId";
$updateStmt = $db->prepare($updateSql);

foreach ($binds as $key => $bind) {
$updateStmt->bindValue($key, $bind['value'], $bind['type']);
}

$updateStmt->bindValue(':userId', (int) $user['id'], SQLITE3_INTEGER);
$updateResult = $updateStmt->execute();

if ($updateResult) {
echo json_encode([
'success' => true,
'title' => 'Updated',
'message' => 'Budget settings updated successfully.'
]);
} else {
echo json_encode([
'success' => false,
'title' => 'Database error',
'message' => 'Failed to update budget settings.'
]);
}

$db->close();

?>
Loading