diff --git a/automation/send-invoice.php b/automation/send-invoice.php index a0d3f10..a0d76dd 100644 --- a/automation/send-invoice.php +++ b/automation/send-invoice.php @@ -1,6 +1,6 @@ error('DB Connection failed: ' . $e->getMessage()); } -$stmt = $pdo->prepare("SELECT value FROM settings WHERE name = :name"); -$stmt->execute(['name' => 'email']); -$row = $stmt->fetch(); -if ($row) { - $supportEmail = $row['value']; -} else { - $supportEmail = 'default-support@example.com'; -} +$settingsStmt = $pdo->query(" + SELECT name, value FROM settings + WHERE name IN ('email', 'phone', 'company_name') +"); +$settings = $settingsStmt->fetchAll(PDO::FETCH_KEY_PAIR); -$stmt = $pdo->prepare("SELECT value FROM settings WHERE name = :name"); -$stmt->execute(['name' => 'phone']); -$row = $stmt->fetch(); -if ($row) { - $supportPhoneNumber = $row['value']; -} else { - $supportPhoneNumber = '+1.23456789'; -} - -$stmt = $pdo->prepare("SELECT value FROM settings WHERE name = :name"); -$stmt->execute(['name' => 'company_name']); -$row = $stmt->fetch(); -if ($row) { - $registryName = $row['value']; -} else { - $registryName = 'Example Registry LLC'; -} +$supportEmail = $settings['email'] ?? 'default-support@example.com'; +$supportPhoneNumber = $settings['phone'] ?? '+1.23456789'; +$registryName = $settings['company_name'] ?? 'Example Registry LLC'; $previous = date("Y-m", strtotime("first day of previous month")); @@ -59,6 +42,7 @@ try { foreach ($result as $row) { $startDate = $previous . "-01"; + $endDate = date("Y-m-d", strtotime("+1 month", strtotime($startDate))); $combinedStmt = $pdo->prepare(" SELECT COUNT(id) AS trans, @@ -67,29 +51,41 @@ try { statement WHERE registrar_id = :registrarId AND - date BETWEEN :startDate AND LAST_DAY(:startDate) + date >= :startDate AND date < :endDate "); $combinedStmt->bindParam(':registrarId', $row['id'], PDO::PARAM_INT); $combinedStmt->bindParam(':startDate', $startDate); + $combinedStmt->bindParam(':endDate', $endDate); $combinedStmt->execute(); $combinedResult = $combinedStmt->fetch(PDO::FETCH_ASSOC); - $transactionsCount = $combinedResult['trans'] ?? 0; - $totalAmount = $combinedResult['total'] ?? '0'; + $refundStmt = $pdo->prepare(" + SELECT GROUP_CONCAT(description SEPARATOR '\n') AS refund_list, + SUM(amount) AS refund_total + FROM payment_history + WHERE registrar_id = :registrarId + AND date BETWEEN :startDate AND LAST_DAY(:startDate) + AND description LIKE '%provides a credit%' + "); + $refundStmt->bindParam(':registrarId', $row['id'], PDO::PARAM_INT); + $refundStmt->bindParam(':startDate', $startDate); + $refundStmt->execute(); + $refundRow = $refundStmt->fetch(PDO::FETCH_ASSOC); + $refundTotal = $refundRow['refund_total'] ?? 0; + $refundDetailsRaw = $refundRow['refund_list'] ?? ''; - if ($transactionsCount > 0) { + $transactionsCount = $combinedResult['trans'] ?? 0; + $totalAmount = ($combinedResult['total'] ?? 0) - $refundTotal; + + if ($transactionsCount > 0 && $totalAmount > 0) { // Prepare and execute insert statement $insertStmt = $pdo->prepare("INSERT INTO invoices (registrar_id, billing_contact_id, issue_date, due_date, total_amount, payment_status, created_at) VALUES (:registrarId, :billingContactId, :issueDate, :dueDate, :totalAmount, :paymentStatus, :createdAt)"); - - $currentDateTime = new DateTime(); // Current date and time - $currentDateTimeFormatted = $currentDateTime->format('Y-m-d H:i:s.u'); // Format with microseconds - $dueDateTime = (new DateTime())->modify('+30 days'); // Current date and time plus 30 days - $dueDateTimeFormatted = $dueDateTime->format('Y-m-d H:i:s.u'); // Format with microseconds + $currentDateTime = new DateTimeImmutable(); + $dueDateTime = $currentDateTime->modify('+30 days'); - // Truncate microseconds to milliseconds - $currentDateTimeMilliseconds = substr($currentDateTimeFormatted, 0, 23); - $dueDateTimeMilliseconds = substr($dueDateTimeFormatted, 0, 23); + $currentDateTimeMilliseconds = $currentDateTime->format('Y-m-d H:i:s.v'); + $dueDateTimeMilliseconds = $dueDateTime->format('Y-m-d H:i:s.v'); $paymentStatus = 'unpaid'; @@ -112,9 +108,16 @@ try { $stmt->bindParam(':invoiceNumber', $invoiceNumber, PDO::PARAM_INT); $stmt->execute(); + $log->info("Generated invoice {$invoiceIdFormatted} for registrar ID {$row['id']} ({$row['registrar_name']}) - Amount: {$totalAmount}"); + $issueDate = date("Y-m-d"); $dueDate = date("Y-m-d", strtotime("+30 days")); + if (empty($row['billing_email'])) { + $log->warning("Missing billing email for registrar ID {$row['id']}, skipping email."); + continue; + } + // Prepare the email content $subject = "New Invoice Notification - " . $issueDate; $body = "Dear " . $row['registrar_name'] . ",\n\n" . @@ -123,14 +126,21 @@ try { "- Invoice Number: " . $invoiceIdFormatted . "\n" . "- Issue Date: " . $issueDate . "\n" . "- Due Date: " . $dueDate . "\n" . - "- Total Amount: " . $totalAmount . "\n\n" . - "The invoice is available in your account for review and payment. Please ensure that the payment is made by the due date to avoid any late fees or service interruptions.\n\n" . - "Should you have any questions or require further assistance, please do not hesitate to contact us at {$supportEmail}.\n\n" . - "Thank you for your prompt attention to this matter.\n\n" . - "Warm regards,\n\n" . - "{$registryName}\n" . - "{$supportEmail}\n" . - "{$supportPhoneNumber}"; + "- Total Amount: " . $totalAmount . "\n\n"; + + if ($refundTotal > 0) { + $body .= "- Credits This Period: -" . $refundTotal . "\n"; + $body .= " (Details below)\n\n"; + $body .= $refundDetailsRaw . "\n\n"; + } + + $body .= "The invoice is available in your account for review and payment. Please ensure that the payment is made by the due date to avoid any late fees or service interruptions.\n\n" . + "Should you have any questions or require further assistance, please do not hesitate to contact us at {$supportEmail}.\n\n" . + "Thank you for your prompt attention to this matter.\n\n" . + "Warm regards,\n\n" . + "{$registryName}\n" . + "{$supportEmail}\n" . + "{$supportPhoneNumber}"; // Prepare the data array for the cURL request $data = [ diff --git a/cp/app/Controllers/ApplicationsController.php b/cp/app/Controllers/ApplicationsController.php index 73040e9..e03c2cb 100644 --- a/cp/app/Controllers/ApplicationsController.php +++ b/cp/app/Controllers/ApplicationsController.php @@ -166,8 +166,8 @@ class ApplicationsController extends Controller // Validate that acceptedDate is before notAfter try { - $acceptedDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['accepted']); - $notAfterDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['notafter']); + $acceptedDate = DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $data['accepted']); + $notAfterDate = DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $data['notafter']); if (!$acceptedDate || !$notAfterDate) { $this->container->get('flash')->addMessage('error', "Invalid date format"); diff --git a/cp/app/Controllers/DomainsController.php b/cp/app/Controllers/DomainsController.php index 4073eba..45f0ef8 100644 --- a/cp/app/Controllers/DomainsController.php +++ b/cp/app/Controllers/DomainsController.php @@ -276,8 +276,8 @@ class DomainsController extends Controller // Validate that acceptedDate is before notAfter try { - $acceptedDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['accepted']); - $notAfterDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['notafter']); + $acceptedDate = DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $data['accepted']); + $notAfterDate = DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $data['notafter']); if (!$acceptedDate || !$notAfterDate) { $this->container->get('flash')->addMessage('error', "Invalid date format"); diff --git a/cp/app/Controllers/FinancialsController.php b/cp/app/Controllers/FinancialsController.php index 22068b3..e57993a 100644 --- a/cp/app/Controllers/FinancialsController.php +++ b/cp/app/Controllers/FinancialsController.php @@ -17,12 +17,12 @@ class FinancialsController extends Controller { return view($response,'admin/financials/transactions.twig'); } - + public function overview(Request $request, Response $response) { return view($response,'admin/financials/overview.twig'); } - + public function invoices(Request $request, Response $response) { return view($response,'admin/financials/invoices.twig'); @@ -73,6 +73,41 @@ class FinancialsController extends Controller $statement = $db->select('SELECT * FROM statement WHERE date BETWEEN ? AND ? AND registrar_id = ?', [ $firstDayPrevMonth, $lastDayPrevMonth, $invoice_details['registrar_id'] ] ); + + $refunds = $db->select(" + SELECT + date, + description, + amount * -1 AS amount -- negate the refund to show as negative + FROM payment_history + WHERE registrar_id = ? + AND date BETWEEN ? AND ? + AND description LIKE '%provides a credit%' + ", [ + $invoice_details['registrar_id'], + $firstDayPrevMonth, + $lastDayPrevMonth + ]); + + foreach ($refunds as &$r) { + $r['domain_name'] = '(refund)'; + $r['command'] = 'REFUND'; + $r['type'] = 'credit'; + + if (preg_match('/domain ([a-z0-9.-]+\.[a-z]{2,})/i', $r['description'], $matchDomain)) { + $r['domain_name'] = $matchDomain[1]; + } + + if (preg_match('/provides a credit (.*)$/i', $r['description'], $matchReason)) { + $r['reason'] = trim($matchReason[1]); + } else { + $r['reason'] = $r['description']; // fallback + } + } + unset($r); + + $allTransactions = array_merge($statement, $refunds); + usort($allTransactions, fn($a, $b) => strtotime($a['date']) <=> strtotime($b['date'])); $vatCalculator = new VatCalculator(); $vatCalculator->setBusinessCountryCode(strtoupper($cc)); @@ -107,7 +142,7 @@ class FinancialsController extends Controller 'billing' => $billing, 'billing_company' => $billing_company, 'billing_vat' => $billing_vat, - 'statement' => $statement, + 'statement' => $allTransactions, 'company_name' => $company_name, 'address' => $address, 'address2' => $address2, diff --git a/cp/resources/views/admin/financials/viewInvoice.twig b/cp/resources/views/admin/financials/viewInvoice.twig index a3a877e..2c3827b 100644 --- a/cp/resources/views/admin/financials/viewInvoice.twig +++ b/cp/resources/views/admin/financials/viewInvoice.twig @@ -85,7 +85,12 @@
{{ item.command }} {{ item.domain_name }}
+
+ {{ item.command }} {{ item.domain_name }}
+ {% if item.type == 'credit' and item.reason is defined %}
+
{{ item.reason }}
+ {% endif %}
+