| Server IP : 173.209.174.21 / Your IP : 216.73.216.89 Web Server : Apache/2.4.58 (Ubuntu) System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : nodor ( 1000) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/wcfs/tabs/ |
Upload File : |
<?php
require_once __DIR__ . '/donation_helpers.php';
function ensureGiftCardTables($conn)
{
$cardsSql = "
CREATE TABLE IF NOT EXISTS gift_cards (
id INT(11) NOT NULL AUTO_INCREMENT,
card_label VARCHAR(255) NOT NULL,
card_number VARCHAR(255) NOT NULL DEFAULT '',
initial_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
notes TEXT DEFAULT NULL,
issued_date DATE NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_gift_cards_label (card_label),
KEY idx_gift_cards_issued_date (issued_date)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci
";
if (!$conn->query($cardsSql)) {
throw new RuntimeException('Could not create the gift cards table: ' . $conn->error);
}
upgradeGiftCardsTable($conn);
$transactionsSql = "
CREATE TABLE IF NOT EXISTS gift_card_transactions (
id INT(11) NOT NULL AUTO_INCREMENT,
gift_card_id INT(11) NOT NULL,
transaction_date DATE NOT NULL,
item_description VARCHAR(255) NOT NULL,
quantity DECIMAL(10,2) NOT NULL DEFAULT 1.00,
unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
taxable TINYINT(1) NOT NULL DEFAULT 0,
notes TEXT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_gift_card_transactions_card (gift_card_id),
KEY idx_gift_card_transactions_date (transaction_date)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci
";
if (!$conn->query($transactionsSql)) {
throw new RuntimeException('Could not create the gift card transactions table: ' . $conn->error);
}
}
function giftCardColumnExists($conn, $columnName)
{
$safeColumn = $conn->real_escape_string($columnName);
$result = $conn->query("SHOW COLUMNS FROM gift_cards LIKE '" . $safeColumn . "'");
if (!$result) {
throw new RuntimeException('Could not inspect the gift cards table: ' . $conn->error);
}
$exists = $result->num_rows > 0;
$result->close();
return $exists;
}
function upgradeGiftCardsTable($conn)
{
if (!giftCardColumnExists($conn, 'expiration_days')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN expiration_days INT(11) NOT NULL DEFAULT 180 AFTER issued_date")) {
throw new RuntimeException('Could not add expiration days to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'expiration_date')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN expiration_date DATE DEFAULT NULL AFTER expiration_days")) {
throw new RuntimeException('Could not add expiration date to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'is_donation')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN is_donation TINYINT(1) NOT NULL DEFAULT 0 AFTER initial_amount")) {
throw new RuntimeException('Could not add donation flag to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'donation_recipient')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN donation_recipient VARCHAR(255) NOT NULL DEFAULT '' AFTER is_donation")) {
throw new RuntimeException('Could not add donation recipient to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'donation_qbo_account')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN donation_qbo_account VARCHAR(255) NOT NULL DEFAULT '' AFTER donation_recipient")) {
throw new RuntimeException('Could not add donation QBO account to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'donation_entry_id')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN donation_entry_id INT(11) DEFAULT NULL AFTER donation_qbo_account")) {
throw new RuntimeException('Could not add linked donation entry id to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'is_archived')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN is_archived TINYINT(1) NOT NULL DEFAULT 0 AFTER donation_entry_id")) {
throw new RuntimeException('Could not add archived flag to gift cards: ' . $conn->error);
}
}
if (!giftCardColumnExists($conn, 'archived_at')) {
if (!$conn->query("ALTER TABLE gift_cards ADD COLUMN archived_at DATETIME DEFAULT NULL AFTER is_archived")) {
throw new RuntimeException('Could not add archived timestamp to gift cards: ' . $conn->error);
}
}
if (!$conn->query("UPDATE gift_cards SET expiration_date = DATE_ADD(issued_date, INTERVAL expiration_days DAY) WHERE expiration_date IS NULL")) {
throw new RuntimeException('Could not backfill gift card expiration dates: ' . $conn->error);
}
}
function giftCardExpirationOptions()
{
return array(90, 180, 360);
}
function normalizeGiftCardExpirationDays($days)
{
$days = (int) $days;
if (!in_array($days, giftCardExpirationOptions(), true)) {
return 180;
}
return $days;
}
function buildGiftCardSelectSql($expired)
{
$where = $expired ? 'expiration_date <= CURDATE()' : 'expiration_date > CURDATE()';
return "
SELECT id, card_label, card_number, initial_amount, issued_date, expiration_days, expiration_date, is_donation, donation_recipient, donation_qbo_account, donation_entry_id, notes, created_at
FROM gift_cards
WHERE {$where}
AND is_archived = 0
ORDER BY expiration_date ASC, issued_date DESC, id DESC
";
}
function normalizeGiftCardExportScope($scope)
{
$scope = trim((string) $scope);
if (!in_array($scope, array('month', 'range', 'all'), true)) {
return 'month';
}
return $scope;
}
function normalizeGiftCardMonth($month)
{
$month = trim((string) $month);
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
return date('Y-m');
}
return $month;
}
function giftCardMonthRange($month)
{
$month = normalizeGiftCardMonth($month);
$start = $month . '-01';
$end = date('Y-m-t', strtotime($start));
return array($start, $end);
}
function normalizeGiftCardDate($date)
{
$date = trim((string) $date);
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return '';
}
return $date;
}
function giftCardFilterClause($scope, $month, $startDate, $endDate, &$params, &$types)
{
$scope = normalizeGiftCardExportScope($scope);
$params = array();
$types = '';
if ($scope === 'all') {
return '';
}
if ($scope === 'range') {
$startDate = normalizeGiftCardDate($startDate);
$endDate = normalizeGiftCardDate($endDate);
if ($startDate === '' || $endDate === '') {
return '';
}
$params[] = $startDate;
$params[] = $endDate;
$types .= 'ss';
return ' AND issued_date BETWEEN ? AND ?';
}
list($monthStart, $monthEnd) = giftCardMonthRange($month);
$params[] = $monthStart;
$params[] = $monthEnd;
$types .= 'ss';
return ' AND issued_date BETWEEN ? AND ?';
}
function fetchGiftCards($conn, $taxRate, $scope, $month, $startDate, $endDate)
{
ensureGiftCardTables($conn);
return fetchGiftCardsByVisibilityAndFilter($conn, $taxRate, false, false, $scope, $month, $startDate, $endDate);
}
function fetchExpiredGiftCards($conn, $taxRate)
{
ensureGiftCardTables($conn);
return fetchGiftCardsByVisibilityAndFilter($conn, $taxRate, true, false, 'all', '', '', '');
}
function fetchArchivedGiftCards($conn, $taxRate)
{
ensureGiftCardTables($conn);
return fetchGiftCardsByVisibilityAndFilter($conn, $taxRate, false, true, 'all', '', '', '');
}
function fetchGiftCardsByVisibilityAndFilter($conn, $taxRate, $expired, $archived, $scope, $month, $startDate, $endDate)
{
ensureGiftCardTables($conn);
$whereParts = array();
if ($archived) {
$whereParts[] = 'is_archived = 1';
} else {
$whereParts[] = 'is_archived = 0';
$whereParts[] = $expired ? 'expiration_date <= CURDATE()' : 'expiration_date > CURDATE()';
}
$params = array();
$types = '';
$dateClause = giftCardFilterClause($scope, $month, $startDate, $endDate, $params, $types);
if ($dateClause !== '') {
$whereParts[] = substr($dateClause, 5);
}
$orderBy = $archived
? 'archived_at DESC, expiration_date DESC, id DESC'
: ($expired ? 'expiration_date ASC, issued_date DESC, id DESC' : 'issued_date DESC, id DESC');
$sql = "
SELECT id, card_label, card_number, initial_amount, issued_date, expiration_days, expiration_date, is_donation, donation_recipient, donation_qbo_account, donation_entry_id, notes, archived_at, created_at
FROM gift_cards
WHERE " . implode(' AND ', $whereParts) . "
ORDER BY {$orderBy}
";
if ($types === '') {
$result = $conn->query($sql);
if (!$result) {
throw new RuntimeException('Could not load gift cards: ' . $conn->error);
}
$rows = $result->fetch_all(MYSQLI_ASSOC);
} else {
$stmt = $conn->prepare($sql);
if (!$stmt) {
throw new RuntimeException('Could not prepare the gift card query: ' . $conn->error);
}
$bind = array($types);
foreach ($params as $index => $param) {
$bind[] = &$params[$index];
}
call_user_func_array(array($stmt, 'bind_param'), $bind);
$stmt->execute();
$result = $stmt->get_result();
$rows = $result ? $result->fetch_all(MYSQLI_ASSOC) : array();
$stmt->close();
}
foreach ($rows as &$row) {
$row['spent_total'] = giftCardSpentTotal($conn, (int) $row['id'], $taxRate);
$row['remaining_balance'] = giftCardRemainingBalance($row, $row['spent_total']);
$row['amount_owed'] = giftCardAmountOwed($row['remaining_balance']);
}
unset($row);
return $rows;
}
function addGiftCard($conn, $cardLabel, $cardNumber, $initialAmount, $issuedDate, $expirationDays, $notes, $isDonation, $donationRecipient, $donationQboAccount)
{
ensureGiftCardTables($conn);
$cardLabel = trim((string) $cardLabel);
$cardNumber = trim((string) $cardNumber);
$initialAmount = trim((string) $initialAmount);
$issuedDate = trim((string) $issuedDate);
$expirationDays = normalizeGiftCardExpirationDays($expirationDays);
$notes = trim((string) $notes);
$isDonation = (int) $isDonation === 1 ? 1 : 0;
$donationRecipient = trim((string) $donationRecipient);
$donationQboAccount = trim((string) $donationQboAccount);
if ($cardLabel === '') {
throw new RuntimeException('Gift card label is required.');
}
if ($initialAmount === '' || !is_numeric($initialAmount) || (float) $initialAmount < 0) {
throw new RuntimeException('Please enter a valid starting amount.');
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $issuedDate)) {
throw new RuntimeException('Please enter a valid issue date.');
}
if ($isDonation === 1) {
if ($donationRecipient === '') {
throw new RuntimeException('Donation recipient / organization is required when this gift card was donated.');
}
if ($donationQboAccount === '') {
throw new RuntimeException('Donation QBO account/category is required when this gift card was donated.');
}
}
$initialAmountValue = number_format((float) $initialAmount, 2, '.', '');
$expirationDate = date('Y-m-d', strtotime($issuedDate . ' + ' . $expirationDays . ' days'));
$donationEntryId = null;
if ($isDonation === 1) {
$donationNotes = 'Gift card donation';
if ($cardNumber !== '') {
$donationNotes .= ' - Card #' . $cardNumber;
}
if ($notes !== '') {
$donationNotes .= ' - ' . $notes;
}
addDonation(
$conn,
$issuedDate,
$donationRecipient,
$donationQboAccount,
'Gift Card - ' . $cardLabel,
'1.00',
$initialAmountValue,
$donationNotes
);
$donationEntryId = (int) $conn->insert_id;
}
$stmt = $conn->prepare("
INSERT INTO gift_cards (
card_label,
card_number,
initial_amount,
issued_date,
expiration_days,
expiration_date,
is_donation,
donation_recipient,
donation_qbo_account,
donation_entry_id,
notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->bind_param('ssssisisiss', $cardLabel, $cardNumber, $initialAmountValue, $issuedDate, $expirationDays, $expirationDate, $isDonation, $donationRecipient, $donationQboAccount, $donationEntryId, $notes);
$stmt->execute();
$stmt->close();
}
function fetchGiftCard($conn, $giftCardId, $taxRate)
{
ensureGiftCardTables($conn);
$stmt = $conn->prepare("
SELECT id, card_label, card_number, initial_amount, issued_date, expiration_days, expiration_date, is_donation, donation_recipient, donation_qbo_account, donation_entry_id, notes, created_at
FROM gift_cards
WHERE id = ?
AND is_archived = 0
LIMIT 1
");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$result = $stmt->get_result();
$card = $result ? $result->fetch_assoc() : null;
$stmt->close();
if (!$card) {
throw new RuntimeException('Gift card not found.');
}
$transactions = fetchGiftCardTransactions($conn, $giftCardId, $taxRate);
$card['transactions'] = $transactions;
$card['spent_total'] = giftCardTransactionsTotal($transactions);
$card['remaining_balance'] = giftCardRemainingBalance($card, $card['spent_total']);
$card['amount_owed'] = giftCardAmountOwed($card['remaining_balance']);
return $card;
}
function restoreExpiredGiftCard($conn, $giftCardId, $expirationDays)
{
ensureGiftCardTables($conn);
if ((int) $giftCardId <= 0) {
throw new RuntimeException('Invalid gift card selected.');
}
$expirationDays = normalizeGiftCardExpirationDays($expirationDays);
$today = date('Y-m-d');
$expirationDate = date('Y-m-d', strtotime($today . ' + ' . $expirationDays . ' days'));
$stmt = $conn->prepare("
UPDATE gift_cards
SET expiration_days = ?, expiration_date = ?
WHERE id = ?
LIMIT 1
");
$stmt->bind_param('isi', $expirationDays, $expirationDate, $giftCardId);
$stmt->execute();
$stmt->close();
}
function restoreGiftCardOnly($conn, $giftCardId)
{
ensureGiftCardTables($conn);
if ((int) $giftCardId <= 0) {
throw new RuntimeException('Invalid gift card selected.');
}
$stmt = $conn->prepare("UPDATE gift_cards SET is_archived = 0, archived_at = NULL WHERE id = ? LIMIT 1");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$stmt->close();
}
function archiveGiftCardOnly($conn, $giftCardId)
{
ensureGiftCardTables($conn);
if ((int) $giftCardId <= 0) {
throw new RuntimeException('Invalid gift card selected.');
}
$archivedAt = date('Y-m-d H:i:s');
$stmt = $conn->prepare("UPDATE gift_cards SET is_archived = 1, archived_at = ? WHERE id = ? LIMIT 1");
$stmt->bind_param('si', $archivedAt, $giftCardId);
$stmt->execute();
$stmt->close();
}
function archiveGiftCardAndLinkedDonation($conn, $giftCardId)
{
ensureGiftCardTables($conn);
$stmt = $conn->prepare("SELECT donation_entry_id FROM gift_cards WHERE id = ? LIMIT 1");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result ? $result->fetch_assoc() : null;
$stmt->close();
if ($row && !empty($row['donation_entry_id'])) {
archiveDonation($conn, (int) $row['donation_entry_id']);
}
archiveGiftCardOnly($conn, $giftCardId);
}
function restoreGiftCardAndLinkedDonation($conn, $giftCardId)
{
ensureGiftCardTables($conn);
$stmt = $conn->prepare("SELECT donation_entry_id FROM gift_cards WHERE id = ? LIMIT 1");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result ? $result->fetch_assoc() : null;
$stmt->close();
if ($row && !empty($row['donation_entry_id'])) {
restoreDonation($conn, (int) $row['donation_entry_id']);
}
restoreGiftCardOnly($conn, $giftCardId);
}
function deleteGiftCardOnly($conn, $giftCardId)
{
ensureGiftCardTables($conn);
if ((int) $giftCardId <= 0) {
throw new RuntimeException('Invalid gift card selected.');
}
$stmt = $conn->prepare("DELETE FROM gift_card_transactions WHERE gift_card_id = ?");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$stmt->close();
$stmt = $conn->prepare("DELETE FROM gift_cards WHERE id = ? LIMIT 1");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$stmt->close();
}
function deleteGiftCardAndLinkedDonation($conn, $giftCardId)
{
ensureGiftCardTables($conn);
$stmt = $conn->prepare("SELECT donation_entry_id FROM gift_cards WHERE id = ? LIMIT 1");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result ? $result->fetch_assoc() : null;
$stmt->close();
$donationEntryId = $row && !empty($row['donation_entry_id']) ? (int) $row['donation_entry_id'] : 0;
deleteGiftCardOnly($conn, $giftCardId);
if ($donationEntryId > 0) {
deleteDonation($conn, $donationEntryId);
}
}
function fetchGiftCardTransactions($conn, $giftCardId, $taxRate)
{
ensureGiftCardTables($conn);
$stmt = $conn->prepare("
SELECT id, gift_card_id, transaction_date, item_description, quantity, unit_price, taxable, notes, created_at
FROM gift_card_transactions
WHERE gift_card_id = ?
ORDER BY transaction_date DESC, id DESC
");
$stmt->bind_param('i', $giftCardId);
$stmt->execute();
$result = $stmt->get_result();
$rows = $result ? $result->fetch_all(MYSQLI_ASSOC) : array();
$stmt->close();
foreach ($rows as &$row) {
$row['subtotal'] = giftCardTransactionSubtotal($row);
$row['tax_amount'] = giftCardTransactionTax($row, $taxRate);
$row['line_total'] = $row['subtotal'] + $row['tax_amount'];
}
unset($row);
return $rows;
}
function addGiftCardTransaction($conn, $giftCardId, $transactionDate, $itemDescription, $quantity, $unitPrice, $taxable, $notes)
{
ensureGiftCardTables($conn);
if ((int) $giftCardId <= 0) {
throw new RuntimeException('Invalid gift card selected.');
}
$transactionDate = trim((string) $transactionDate);
$itemDescription = trim((string) $itemDescription);
$quantity = trim((string) $quantity);
$unitPrice = trim((string) $unitPrice);
$notes = trim((string) $notes);
$taxable = (int) $taxable === 1 ? 1 : 0;
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $transactionDate)) {
throw new RuntimeException('Please enter a valid transaction date.');
}
if ($itemDescription === '') {
throw new RuntimeException('Item description is required.');
}
if ($quantity === '' || !is_numeric($quantity) || (float) $quantity <= 0) {
throw new RuntimeException('Please enter a valid quantity greater than zero.');
}
if ($unitPrice === '' || !is_numeric($unitPrice) || (float) $unitPrice < 0) {
throw new RuntimeException('Please enter a valid price.');
}
$quantityValue = number_format((float) $quantity, 2, '.', '');
$unitPriceValue = number_format((float) $unitPrice, 2, '.', '');
$stmt = $conn->prepare("
INSERT INTO gift_card_transactions (
gift_card_id,
transaction_date,
item_description,
quantity,
unit_price,
taxable,
notes
) VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->bind_param('issssis', $giftCardId, $transactionDate, $itemDescription, $quantityValue, $unitPriceValue, $taxable, $notes);
$stmt->execute();
$stmt->close();
}
function updateGiftCardInfo($conn, $giftCardId, $cardLabel, $cardNumber, $notes)
{
ensureGiftCardTables($conn);
if ((int) $giftCardId <= 0) {
throw new RuntimeException('Invalid gift card selected.');
}
$cardLabel = trim((string) $cardLabel);
$cardNumber = trim((string) $cardNumber);
$notes = trim((string) $notes);
if ($cardLabel === '') {
throw new RuntimeException('Gift card label is required.');
}
$stmt = $conn->prepare("
UPDATE gift_cards
SET card_label = ?, card_number = ?, notes = ?
WHERE id = ?
LIMIT 1
");
$stmt->bind_param('sssi', $cardLabel, $cardNumber, $notes, $giftCardId);
$stmt->execute();
$stmt->close();
}
function updateGiftCardTransaction($conn, $transactionId, $quantity, $unitPrice, $taxable, $notes)
{
ensureGiftCardTables($conn);
if ((int) $transactionId <= 0) {
throw new RuntimeException('Invalid gift card transaction selected.');
}
$quantity = trim((string) $quantity);
$unitPrice = trim((string) $unitPrice);
$notes = trim((string) $notes);
$taxable = (int) $taxable === 1 ? 1 : 0;
if ($quantity === '' || !is_numeric($quantity) || (float) $quantity <= 0) {
throw new RuntimeException('Please enter a valid quantity greater than zero.');
}
if ($unitPrice === '' || !is_numeric($unitPrice) || (float) $unitPrice < 0) {
throw new RuntimeException('Please enter a valid price.');
}
$quantityValue = number_format((float) $quantity, 2, '.', '');
$unitPriceValue = number_format((float) $unitPrice, 2, '.', '');
$stmt = $conn->prepare("
UPDATE gift_card_transactions
SET quantity = ?, unit_price = ?, taxable = ?, notes = ?
WHERE id = ?
LIMIT 1
");
$stmt->bind_param('ssisi', $quantityValue, $unitPriceValue, $taxable, $notes, $transactionId);
$stmt->execute();
$stmt->close();
}
function deleteGiftCardTransaction($conn, $transactionId)
{
ensureGiftCardTables($conn);
if ((int) $transactionId <= 0) {
throw new RuntimeException('Invalid gift card transaction selected.');
}
$stmt = $conn->prepare("DELETE FROM gift_card_transactions WHERE id = ? LIMIT 1");
$stmt->bind_param('i', $transactionId);
$stmt->execute();
$stmt->close();
}
function giftCardTransactionSubtotal($row)
{
$quantity = isset($row['quantity']) ? (float) $row['quantity'] : 0;
$unitPrice = isset($row['unit_price']) ? (float) $row['unit_price'] : 0;
return $quantity * $unitPrice;
}
function giftCardTransactionTax($row, $taxRate)
{
if (empty($row['taxable'])) {
return 0.0;
}
return giftCardTransactionSubtotal($row) * (float) $taxRate;
}
function giftCardTransactionsTotal($rows)
{
$total = 0.0;
foreach ($rows as $row) {
$total += isset($row['line_total']) ? (float) $row['line_total'] : 0.0;
}
return $total;
}
function giftCardSpentTotal($conn, $giftCardId, $taxRate)
{
$transactions = fetchGiftCardTransactions($conn, $giftCardId, $taxRate);
return giftCardTransactionsTotal($transactions);
}
function giftCardRemainingBalance($card, $spentTotal)
{
$initialAmount = isset($card['initial_amount']) ? (float) $card['initial_amount'] : 0.0;
return $initialAmount - (float) $spentTotal;
}
function giftCardAmountOwed($remainingBalance)
{
$remainingBalance = (float) $remainingBalance;
if ($remainingBalance >= 0) {
return 0.0;
}
return abs($remainingBalance);
}
function giftCardCounts($conn)
{
ensureGiftCardTables($conn);
$counts = array(
'active' => 0,
'expired' => 0,
'archived' => 0,
);
$queries = array(
'active' => "SELECT COUNT(*) AS total FROM gift_cards WHERE is_archived = 0 AND expiration_date > CURDATE()",
'expired' => "SELECT COUNT(*) AS total FROM gift_cards WHERE is_archived = 0 AND expiration_date <= CURDATE()",
'archived' => "SELECT COUNT(*) AS total FROM gift_cards WHERE is_archived = 1",
);
foreach ($queries as $key => $sql) {
$result = $conn->query($sql);
if (!$result) {
throw new RuntimeException('Could not load gift card counts: ' . $conn->error);
}
$row = $result->fetch_assoc();
$counts[$key] = $row ? (int) $row['total'] : 0;
}
return $counts;
}
function quickFindGiftCard($conn, $lookup, $taxRate)
{
ensureGiftCardTables($conn);
$lookup = trim((string) $lookup);
if ($lookup === '') {
return null;
}
$like = '%' . $lookup . '%';
$stmt = $conn->prepare("
SELECT id
FROM gift_cards
WHERE is_archived = 0
AND (card_label LIKE ? OR card_number LIKE ?)
ORDER BY expiration_date ASC, id DESC
LIMIT 1
");
if (!$stmt) {
throw new RuntimeException('Could not search gift cards: ' . $conn->error);
}
$stmt->bind_param('ss', $like, $like);
$stmt->execute();
$result = $stmt->get_result();
$row = $result ? $result->fetch_assoc() : null;
$stmt->close();
if (!$row) {
return null;
}
return fetchGiftCard($conn, (int) $row['id'], $taxRate);
}
function soonExpiringGiftCards($conn, $taxRate, $daysAhead)
{
ensureGiftCardTables($conn);
$daysAhead = (int) $daysAhead;
if ($daysAhead <= 0) {
$daysAhead = 14;
}
$stmt = $conn->prepare("
SELECT id, card_label, card_number, initial_amount, issued_date, expiration_days, expiration_date, is_donation, donation_recipient, donation_qbo_account, donation_entry_id, notes, archived_at, created_at
FROM gift_cards
WHERE is_archived = 0
AND expiration_date > CURDATE()
AND expiration_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
ORDER BY expiration_date ASC, id ASC
");
if (!$stmt) {
throw new RuntimeException('Could not load soon-to-expire gift cards: ' . $conn->error);
}
$stmt->bind_param('i', $daysAhead);
$stmt->execute();
$result = $stmt->get_result();
$rows = $result ? $result->fetch_all(MYSQLI_ASSOC) : array();
$stmt->close();
foreach ($rows as &$row) {
$row['spent_total'] = giftCardSpentTotal($conn, (int) $row['id'], $taxRate);
$row['remaining_balance'] = giftCardRemainingBalance($row, $row['spent_total']);
$row['amount_owed'] = giftCardAmountOwed($row['remaining_balance']);
$row['days_until_expiration'] = (int) floor((strtotime($row['expiration_date']) - strtotime(date('Y-m-d'))) / 86400);
}
unset($row);
return $rows;
}
function transferableGiftCards($conn, $taxRate, $excludeGiftCardId)
{
$rows = fetchGiftCards($conn, $taxRate, 'all', '', '', '');
$excludeGiftCardId = (int) $excludeGiftCardId;
return array_values(array_filter($rows, function ($row) use ($excludeGiftCardId) {
return (int) $row['id'] !== $excludeGiftCardId;
}));
}
function transferGiftCardBalance($conn, $sourceGiftCardId, $targetGiftCardId)
{
ensureGiftCardTables($conn);
$sourceGiftCardId = (int) $sourceGiftCardId;
$targetGiftCardId = (int) $targetGiftCardId;
if ($sourceGiftCardId <= 0 || $targetGiftCardId <= 0 || $sourceGiftCardId === $targetGiftCardId) {
throw new RuntimeException('Choose two different gift cards for the transfer.');
}
$stmt = $conn->prepare("
SELECT id, card_label, initial_amount, is_archived
FROM gift_cards
WHERE id IN (?, ?)
");
if (!$stmt) {
throw new RuntimeException('Could not load gift cards for transfer: ' . $conn->error);
}
$stmt->bind_param('ii', $sourceGiftCardId, $targetGiftCardId);
$stmt->execute();
$result = $stmt->get_result();
$cards = array();
while ($row = $result->fetch_assoc()) {
$cards[(int) $row['id']] = $row;
}
$stmt->close();
if (!isset($cards[$sourceGiftCardId]) || !isset($cards[$targetGiftCardId])) {
throw new RuntimeException('One of the selected gift cards could not be found.');
}
if (!empty($cards[$sourceGiftCardId]['is_archived']) || !empty($cards[$targetGiftCardId]['is_archived'])) {
throw new RuntimeException('Archived gift cards cannot be used for a transfer.');
}
$taxRate = 0.0;
$sourceCard = fetchGiftCard($conn, $sourceGiftCardId, $taxRate);
$targetCard = fetchGiftCard($conn, $targetGiftCardId, $taxRate);
$remainingBalance = (float) $sourceCard['remaining_balance'];
if ($remainingBalance <= 0) {
throw new RuntimeException('The source gift card does not have a remaining balance to transfer.');
}
$newTargetAmount = (float) $targetCard['initial_amount'] + $remainingBalance;
$conn->begin_transaction();
try {
$newTargetAmountValue = number_format($newTargetAmount, 2, '.', '');
$targetNote = trim((string) $targetCard['notes']);
$transferNote = 'Received balance transfer of $' . number_format($remainingBalance, 2) . ' from ' . $sourceCard['card_label'];
$targetNote = $targetNote === '' ? $transferNote : ($targetNote . ' | ' . $transferNote);
$stmt = $conn->prepare("
UPDATE gift_cards
SET initial_amount = ?, notes = ?
WHERE id = ?
LIMIT 1
");
$stmt->bind_param('ssi', $newTargetAmountValue, $targetNote, $targetGiftCardId);
$stmt->execute();
$stmt->close();
$sourceNote = trim((string) $sourceCard['notes']);
$sourceTransferNote = 'Transferred remaining balance of $' . number_format($remainingBalance, 2) . ' to ' . $targetCard['card_label'];
$sourceNote = $sourceNote === '' ? $sourceTransferNote : ($sourceNote . ' | ' . $sourceTransferNote);
$stmt = $conn->prepare("
UPDATE gift_cards
SET initial_amount = '0.00', notes = ?
WHERE id = ?
LIMIT 1
");
$stmt->bind_param('si', $sourceNote, $sourceGiftCardId);
$stmt->execute();
$stmt->close();
$conn->commit();
} catch (Exception $e) {
$conn->rollback();
throw $e;
}
return $remainingBalance;
}
function exportGiftCardsCsv($rows)
{
$handle = fopen('php://temp', 'r+');
if ($handle === false) {
throw new RuntimeException('Could not build the gift card export.');
}
fputcsv($handle, array(
'Card Label',
'Card Number',
'Issued Date',
'Expiration Date',
'Initial Amount',
'Spent',
'Remaining Balance',
'Amount Owed',
'Donation Card',
'Donation Recipient',
'Notes',
));
foreach ($rows as $row) {
fputcsv($handle, array(
isset($row['card_label']) ? $row['card_label'] : '',
isset($row['card_number']) ? $row['card_number'] : '',
isset($row['issued_date']) ? $row['issued_date'] : '',
isset($row['expiration_date']) ? $row['expiration_date'] : '',
number_format((float) (isset($row['initial_amount']) ? $row['initial_amount'] : 0), 2, '.', ''),
number_format((float) (isset($row['spent_total']) ? $row['spent_total'] : 0), 2, '.', ''),
number_format((float) (isset($row['remaining_balance']) ? $row['remaining_balance'] : 0), 2, '.', ''),
number_format((float) (isset($row['amount_owed']) ? $row['amount_owed'] : 0), 2, '.', ''),
!empty($row['is_donation']) ? 'Yes' : 'No',
isset($row['donation_recipient']) ? $row['donation_recipient'] : '',
isset($row['notes']) ? $row['notes'] : '',
));
}
rewind($handle);
$csv = stream_get_contents($handle);
fclose($handle);
return $csv === false ? '' : $csv;
}
function giftCardExportFilename($scope, $month, $startDate, $endDate)
{
$scope = normalizeGiftCardExportScope($scope);
if ($scope === 'range') {
$startDate = normalizeGiftCardDate($startDate);
$endDate = normalizeGiftCardDate($endDate);
if ($startDate !== '' && $endDate !== '') {
return 'gift_cards_' . str_replace('-', '_', $startDate) . '_to_' . str_replace('-', '_', $endDate) . '.csv';
}
}
if ($scope === 'all') {
return 'gift_cards_all.csv';
}
return 'gift_cards_' . str_replace('-', '_', normalizeGiftCardMonth($month)) . '.csv';
}