Big improvement on pricing estimation logic in CP

This commit is contained in:
Pinga 2025-02-12 15:01:06 +02:00
parent a9e84a855e
commit 73e19087ae
6 changed files with 305 additions and 287 deletions

View file

@ -756,4 +756,26 @@ class DapiController extends Controller
return $response;
}
public function domainPrice(Request $request, Response $response): Response
{
$params = $request->getQueryParams();
$db = $this->container->get('db');
$domain_name = $params['domain_name'] ?? '';
$date_add = (int) ($params['date_add'] ?? 12);
$command = $params['command'] ?? 'create';
$currency = $params['currency'] ?? 'USD';
$registrar_id = !empty($params['registrar_id']) ? $params['registrar_id'] : ($_SESSION['auth_registrar_id'] ?? null);
$parts = extractDomainAndTLD($domain_name);
$domain_extension = $parts['tld'];
$tld_id = $db->selectValue('SELECT id FROM domain_tld WHERE tld = ?', [ '.'.$domain_extension ]);
$result = getDomainPrice($db, $domain_name, $tld_id, $date_add, $command, $registrar_id, $currency);
$response->getBody()->write(json_encode($result));
return $response->withHeader('Content-Type', 'application/json');
}
}

View file

@ -921,12 +921,20 @@ class DomainsController extends Controller
$registrars = $db->select("SELECT id, clid, name FROM registrar");
if ($_SESSION["auth_roles"] != 0) {
$registrar = true;
$currency = $_SESSION['_currency'] ?? 'USD';
if (!empty($_SESSION['auth_registrar_id'])) {
$currency = $db->selectValue(
'SELECT currency FROM registrar WHERE id = ?',
[$_SESSION['auth_registrar_id']]
) ?? 'USD'; // Default to USD if no result
}
} else {
$registrar = null;
$currency = $_SESSION['_currency'] ?? 'USD';
}
$registry_currency = $_SESSION['registry_currency'] ?? 'USD';
$locale = (isset($_SESSION['_lang']) && !empty($_SESSION['_lang'])) ? $_SESSION['_lang'] : 'en_US';
$currency = $_SESSION['_currency'] ?? 'USD'; // Default to USD if not set
$formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
$formatter->setTextAttribute(\NumberFormatter::CURRENCY_CODE, $currency);
@ -947,6 +955,7 @@ class DomainsController extends Controller
'registrar' => $registrar,
'launch_phases' => $launch_phases,
'currency' => $currency,
'registry_currency' => $registry_currency,
]);
}
@ -1899,9 +1908,18 @@ class DomainsController extends Controller
$registrars = $db->select("SELECT id, clid, name FROM registrar");
if ($_SESSION["auth_roles"] != 0) {
$registrar = true;
$currency = $_SESSION['_currency'] ?? 'USD';
if (!empty($_SESSION['auth_registrar_id'])) {
$currency = $db->selectValue(
'SELECT currency FROM registrar WHERE id = ?',
[$_SESSION['auth_registrar_id']]
) ?? 'USD'; // Default to USD if no result
}
} else {
$registrar = null;
$currency = $_SESSION['_currency'] ?? 'USD';
}
$registry_currency = $_SESSION['registry_currency'] ?? 'USD';
$uri = $request->getUri()->getPath();
@ -1943,7 +1961,6 @@ class DomainsController extends Controller
$maxYears = 10 - $yearsUntilExpiration;
$locale = (isset($_SESSION['_lang']) && !empty($_SESSION['_lang'])) ? $_SESSION['_lang'] : 'en_US';
$currency = $_SESSION['_currency'] ?? 'USD'; // Default to USD if not set
$formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
$formatter->setTextAttribute(\NumberFormatter::CURRENCY_CODE, $currency);
@ -1970,7 +1987,8 @@ class DomainsController extends Controller
'currentUri' => $uri,
'currencySymbol' => $symbol,
'currencyPosition' => $position,
'currency' => $currency
'currency' => $currency,
'registry_currency' => $registry_currency
]);
} else {
// Domain does not exist, redirect to the domains view
@ -2661,12 +2679,20 @@ class DomainsController extends Controller
$registrars = $db->select("SELECT id, clid, name FROM registrar");
if ($_SESSION["auth_roles"] != 0) {
$registrar = true;
$currency = $_SESSION['_currency'] ?? 'USD';
if (!empty($_SESSION['auth_registrar_id'])) {
$currency = $db->selectValue(
'SELECT currency FROM registrar WHERE id = ?',
[$_SESSION['auth_registrar_id']]
) ?? 'USD'; // Default to USD if no result
}
} else {
$registrar = null;
$currency = $_SESSION['_currency'] ?? 'USD';
}
$registry_currency = $_SESSION['registry_currency'] ?? 'USD';
$locale = (isset($_SESSION['_lang']) && !empty($_SESSION['_lang'])) ? $_SESSION['_lang'] : 'en_US';
$currency = $_SESSION['_currency'] ?? 'USD'; // Default to USD if not set
$formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
$formatter->setTextAttribute(\NumberFormatter::CURRENCY_CODE, $currency);
@ -2683,7 +2709,8 @@ class DomainsController extends Controller
'registrar' => $registrar,
'currencySymbol' => $symbol,
'currencyPosition' => $position,
'currency' => $currency
'currency' => $currency,
'registry_currency' => $registry_currency
]);
}

View file

@ -363,6 +363,7 @@ document.addEventListener("DOMContentLoaded", function() {
// Display year value from slider
yearSlider.addEventListener('input', function() {
yearValueDisplay.textContent = `${yearSlider.value} Year${yearSlider.value > 1 ? 's' : ''}`;
updatePrice(); // Call updatePrice() directly when slider moves
});
function createNameserverGroup(count) {
@ -450,96 +451,86 @@ document.addEventListener("DOMContentLoaded", function() {
const priceDisplay = document.getElementById('domainPriceDisplay');
const priceValue = document.getElementById('domainPrice');
function extractTLD(domain) {
const parts = domain.split('.');
// If the domain has more than two segments (e.g., 'test.com.test'), return the last two segments
if (parts.length > 2) {
return parts.slice(-2).join('.').toLowerCase();
}
// If the domain has two or fewer segments (e.g., 'test.test' or 'test'), return the last segment
else {
return parts[parts.length - 1].toLowerCase();
}
}
function getDomainPrice(domain, years, registrarId) {
const tld = extractTLD(domain);
if (!tld) {
return Promise.reject("Invalid TLD");
const currency = "{{ currency }}";
const apiUrl = `/dapi/domain/price?domain_name=${encodeURIComponent(domain)}&date_add=${years * 12}&command=create&registrar_id=${encodeURIComponent(registrarId)}&currency=${encodeURIComponent(currency)}`;
return fetch(apiUrl)
.then(response => response.json())
.then(data => {
// If the response is a raw number (e.g., 0.5), wrap it in an object
if (typeof data === "number") {
data = { price: data };
}
// Regular expression for exact TLD match
const tldRegex = new RegExp(`${tld.toLowerCase()}$`);
// Fetch both promotional pricing and regular pricing
return Promise.all([
fetch(`/api/records/promotion_pricing?join=domain_tld`).then(response => response.json()),
fetch(`/api/records/domain_price?join=domain_tld`).then(response => response.json())
])
.then(([promoData, pricingData]) => {
const today = new Date();
// Check for a valid promotion
const promo = promoData.records.find(record =>
record.tld_id && tldRegex.test(record.tld_id.tld.toLowerCase()) &&
new Date(record.start_date) <= today &&
new Date(record.end_date) >= today &&
(!record.years_of_promotion || record.years_of_promotion >= years)
);
// Find the regular price for the TLD with registrar ID
let tldData = pricingData.records.find(record =>
record.tldid && tldRegex.test(record.tldid.tld.toLowerCase()) &&
record.command === 'create' &&
record.registrar_id == registrarId
);
// If no registrar-specific price found, find the generic price
if (!tldData) {
tldData = pricingData.records.find(record =>
record.tldid && tldRegex.test(record.tldid.tld.toLowerCase()) &&
record.command === 'create' &&
record.registrar_id == null
);
if (!data || typeof data !== "object" || !("price" in data)) {
console.error("Invalid API response structure:", data);
return Promise.reject("Invalid API response structure");
}
if (tldData) {
const priceField = `m${years * 12}`;
let price = parseFloat(tldData[priceField]);
if (!isNaN(price)) {
if (promo) {
// Apply the promotion discount
price -= (price * parseFloat(promo.discount_percentage) / 100);
// Convert price to float safely
const price = parseFloat(data.price);
if (isNaN(price)) {
console.error("Invalid price received:", data.price);
return Promise.reject("Invalid price received");
}
return price;
}
}
return Promise.reject("TLD price not found");
return { price, type: data.type || "regular" };
})
.catch(error => {
console.error("Error fetching pricing data:", error);
return Promise.reject("Error fetching pricing data");
console.error("Error fetching domain price:", error);
return Promise.reject("Error fetching domain price");
});
}
function formatPrice(price) {
switch(window.currencyPosition) {
case 'before':
return `{{ currency }} ${price.toFixed(2)}`;
return `${"{{ currency }}"} ${price.toFixed(2)}`;
case 'after':
return `${price.toFixed(2)} {{ currency }}`;
return `${price.toFixed(2)} ${"{{ currency }}"} `;
default:
return price.toFixed(2);
}
}
function updatePrice() {
if (domainInput.value) {
const registrarId = registrarDropdown.value;
getDomainPrice(domainInput.value, yearInput.value, registrarId).then(price => {
priceValue.innerText = formatPrice(price);
const domainValue = document.getElementById('domainName')?.value.trim() || "";
const registrarId = document.getElementById('registrarDropdown')?.value || "";
const years = parseInt(document.getElementById('registrationYears')?.value, 10) || 1;
if (domainValue) {
getDomainPrice(domainValue, years, registrarId).then(({ price, type }) => {
if (isNaN(price)) {
console.error("Invalid price received:", price);
priceValue.innerText = formatPrice(0.00);
return;
}
// Multiply price by years
const totalPrice = price * years;
priceValue.innerText = formatPrice(totalPrice);
priceDisplay.style.display = 'block';
// Remove existing color classes
priceValue.classList.remove('text-red', 'text-green', 'text-blue');
// Apply appropriate colors based on type
if (type === "promotion") {
priceValue.classList.add('text-green'); // Mark as promotion
priceDisplay.title = "Promotional Price";
} else if (type === "premium") {
priceValue.classList.add('text-red'); // Mark as premium
priceDisplay.title = "Premium Price";
} else {
priceValue.classList.add('text-blue'); // Default regular price
priceDisplay.title = "Regular Price";
}
}).catch(error => {
console.error("Error fetching price:", error);
priceDisplay.style.display = 'none';
});
} else {
priceDisplay.style.display = 'none';
@ -549,6 +540,7 @@ document.addEventListener("DOMContentLoaded", function() {
domainInput.addEventListener('input', updatePrice);
yearInput.addEventListener('input', updatePrice);
registrarDropdown.addEventListener('change', updatePrice);
yearSlider.addEventListener('input', updatePrice);
});
</script>
{% endblock %}

View file

@ -78,117 +78,103 @@ document.addEventListener("DOMContentLoaded", function() {
const yearSlider = document.getElementById('renewalYears');
const yearValueDisplay = document.getElementById('yearValue');
// Display year value from slider
yearSlider.addEventListener('input', function() {
yearValueDisplay.textContent = `${yearSlider.value} Year${yearSlider.value > 1 ? 's' : ''}`;
});
const registrarId = "{{ registrars.id }}"; // Embedded securely in JavaScript
const domainInput = document.getElementById('domainName');
const domainValue = "{{ domain.name }}"; // Get domain from Twig directly
const yearInput = document.getElementById('renewalYears');
const priceDisplay = document.getElementById('domainPriceDisplay');
const priceValue = document.getElementById('domainPrice');
function extractTLD(domain) {
const parts = domain.split('.');
// If the domain has more than two segments (e.g., 'test.com.test'), return the last two segments
if (parts.length > 2) {
return parts.slice(-2).join('.').toLowerCase();
}
// If the domain has two or fewer segments (e.g., 'test.test' or 'test'), return the last segment
else {
return parts[parts.length - 1].toLowerCase();
}
}
// Display year value from slider
yearSlider.addEventListener('input', function() {
yearValueDisplay.textContent = `${yearSlider.value} Year${yearSlider.value > 1 ? 's' : ''}`;
updatePrice(); // Call updatePrice() directly when slider moves
});
function getDomainPrice(domain, years, registrarId) {
const tld = extractTLD(domain);
if (!tld) {
return Promise.reject("Invalid TLD");
const currency = "{{ currency }}";
const apiUrl = `/dapi/domain/price?domain_name=${encodeURIComponent(domain)}&date_add=${years * 12}&command=renew&registrar_id=${encodeURIComponent(registrarId)}&currency=${encodeURIComponent(currency)}`;
return fetch(apiUrl)
.then(response => response.json())
.then(data => {
// If the response is a raw number (e.g., 0.5), wrap it in an object
if (typeof data === "number") {
data = { price: data };
}
// Regular expression for exact TLD match
const tldRegex = new RegExp(`${tld.toLowerCase()}$`);
// Fetch both promotional pricing and regular pricing
return Promise.all([
fetch(`/api/records/promotion_pricing?join=domain_tld`).then(response => response.json()),
fetch(`/api/records/domain_price?join=domain_tld`).then(response => response.json())
])
.then(([promoData, pricingData]) => {
const today = new Date();
// Check for a valid promotion
const promo = promoData.records.find(record =>
record.tld_id && tldRegex.test(record.tld_id.tld.toLowerCase()) &&
new Date(record.start_date) <= today &&
new Date(record.end_date) >= today &&
(!record.years_of_promotion || record.years_of_promotion >= years)
);
// Find the regular price for the TLD with registrar ID
let tldData = pricingData.records.find(record =>
record.tldid && tldRegex.test(record.tldid.tld.toLowerCase()) &&
record.command === 'renew' &&
record.registrar_id == registrarId
);
// If no registrar-specific price found, find the generic price
if (!tldData) {
tldData = pricingData.records.find(record =>
record.tldid && tldRegex.test(record.tldid.tld.toLowerCase()) &&
record.command === 'renew' &&
record.registrar_id == null
);
if (!data || typeof data !== "object" || !("price" in data)) {
console.error("Invalid API response structure:", data);
return Promise.reject("Invalid API response structure");
}
if (tldData) {
const priceField = `m${years * 12}`;
let price = parseFloat(tldData[priceField]);
if (!isNaN(price)) {
if (promo) {
// Apply the promotion discount
price -= (price * parseFloat(promo.discount_percentage) / 100);
// Convert price to float safely
const price = parseFloat(data.price);
if (isNaN(price)) {
console.error("Invalid price received:", data.price);
return Promise.reject("Invalid price received");
}
return price;
}
}
return Promise.reject("TLD price not found");
return { price, type: data.type || "regular" };
})
.catch(error => {
console.error("Error fetching pricing data:", error);
return Promise.reject("Error fetching pricing data");
console.error("Error fetching domain price:", error);
return Promise.reject("Error fetching domain price");
});
}
function formatPrice(price) {
switch(window.currencyPosition) {
case 'before':
return `{{ currency }} ${price.toFixed(2)}`;
return `${"{{ currency }}"} ${price.toFixed(2)}`;
case 'after':
return `${price.toFixed(2)} {{ currency }}`;
return `${price.toFixed(2)} ${"{{ currency }}"} `;
default:
return price.toFixed(2);
}
}
function updatePrice() {
if (domainInput.textContent) {
getDomainPrice(domainInput.textContent, yearInput.value, registrarId).then(price => {
priceValue.innerText = formatPrice(price);
const domainValue = "{{ domain.name }}";
const registrarId = document.getElementById('registrarDropdown')?.value || "";
const years = parseInt(document.getElementById('renewalYears')?.value, 10) || 1;
if (domainValue) {
getDomainPrice(domainValue, years, registrarId).then(({ price, type }) => {
if (isNaN(price)) {
console.error("Invalid price received:", price);
priceValue.innerText = formatPrice(0.00);
return;
}
// Multiply price by years
const totalPrice = price * years;
priceValue.innerText = formatPrice(totalPrice);
priceDisplay.style.display = 'block';
// Remove existing color classes
priceValue.classList.remove('text-red', 'text-green', 'text-blue');
// Apply appropriate colors based on type
if (type === "promotion") {
priceValue.classList.add('text-green'); // Mark as promotion
priceDisplay.title = "Promotional Price";
} else if (type === "premium") {
priceValue.classList.add('text-red'); // Mark as premium
priceDisplay.title = "Premium Price";
} else {
priceValue.classList.add('text-blue'); // Default regular price
priceDisplay.title = "Regular Price";
}
}).catch(error => {
console.error(error);
// Handle the error or display a message as needed
console.error("Error fetching price:", error);
priceDisplay.style.display = 'none';
});
} else {
priceDisplay.style.display = 'none';
}
}
domainInput.addEventListener('input', updatePrice);
yearInput.addEventListener('input', updatePrice);
updatePrice();

View file

@ -91,108 +91,97 @@ document.addEventListener("DOMContentLoaded", function() {
const yearSlider = document.getElementById('transferYears');
const yearValueDisplay = document.getElementById('yearValue');
// Display year value from slider
yearSlider.addEventListener('input', function() {
yearValueDisplay.textContent = `${yearSlider.value} Year${yearSlider.value > 1 ? 's' : ''}`;
});
const domainInput = document.getElementById('domainName');
const yearInput = document.getElementById('transferYears');
const priceDisplay = document.getElementById('domainPriceDisplay');
const priceValue = document.getElementById('domainPrice');
const registrarDropdown = document.getElementById('registrarDropdown');
function extractTLD(domain) {
const parts = domain.split('.');
// If the domain has more than two segments (e.g., 'test.com.test'), return the last two segments
if (parts.length > 2) {
return parts.slice(-2).join('.').toLowerCase();
}
// If the domain has two or fewer segments (e.g., 'test.test' or 'test'), return the last segment
else {
return parts[parts.length - 1].toLowerCase();
}
}
// Display year value from slider
yearSlider.addEventListener('input', function() {
yearValueDisplay.textContent = `${yearSlider.value} Year${yearSlider.value > 1 ? 's' : ''}`;
updatePrice(); // Call updatePrice() directly when slider moves
});
function getDomainPrice(domain, years, registrarId) {
const tld = extractTLD(domain);
if (!tld) {
return Promise.reject("Invalid TLD");
const currency = "{{ currency }}";
const apiUrl = `/dapi/domain/price?domain_name=${encodeURIComponent(domain)}&date_add=${years * 12}&command=transfer&registrar_id=${encodeURIComponent(registrarId)}&currency=${encodeURIComponent(currency)}`;
return fetch(apiUrl)
.then(response => response.json())
.then(data => {
// If the response is a raw number (e.g., 0.5), wrap it in an object
if (typeof data === "number") {
data = { price: data };
}
// Regular expression for exact TLD match
const tldRegex = new RegExp(`${tld.toLowerCase()}$`);
// Fetch both promotional pricing and regular pricing
return Promise.all([
fetch(`/api/records/promotion_pricing?join=domain_tld`).then(response => response.json()),
fetch(`/api/records/domain_price?join=domain_tld`).then(response => response.json())
])
.then(([promoData, pricingData]) => {
const today = new Date();
// Check for a valid promotion
const promo = promoData.records.find(record =>
record.tld_id && tldRegex.test(record.tld_id.tld.toLowerCase()) &&
new Date(record.start_date) <= today &&
new Date(record.end_date) >= today &&
(!record.years_of_promotion || record.years_of_promotion >= years)
);
// Find the regular price for the TLD with registrar ID
let tldData = pricingData.records.find(record =>
record.tldid && tldRegex.test(record.tldid.tld.toLowerCase()) &&
record.command === 'transfer' &&
record.registrar_id == registrarId
);
// If no registrar-specific price found, find the generic price
if (!tldData) {
tldData = pricingData.records.find(record =>
record.tldid && tldRegex.test(record.tldid.tld.toLowerCase()) &&
record.command === 'transfer' &&
record.registrar_id == null
);
if (!data || typeof data !== "object" || !("price" in data)) {
console.error("Invalid API response structure:", data);
return Promise.reject("Invalid API response structure");
}
if (tldData) {
const priceField = `m${years * 12}`;
let price = parseFloat(tldData[priceField]);
if (!isNaN(price)) {
if (promo) {
// Apply the promotion discount
price -= (price * parseFloat(promo.discount_percentage) / 100);
// Convert price to float safely
const price = parseFloat(data.price);
if (isNaN(price)) {
console.error("Invalid price received:", data.price);
return Promise.reject("Invalid price received");
}
return price;
}
}
return Promise.reject("TLD price not found");
return { price, type: data.type || "regular" };
})
.catch(error => {
console.error("Error fetching pricing data:", error);
return Promise.reject("Error fetching pricing data");
console.error("Error fetching domain price:", error);
return Promise.reject("Error fetching domain price");
});
}
function formatPrice(price) {
switch(window.currencyPosition) {
case 'before':
return `{{ currency }} ${price.toFixed(2)}`;
return `${"{{ currency }}"} ${price.toFixed(2)}`;
case 'after':
return `${price.toFixed(2)} {{ currency }}`;
return `${price.toFixed(2)} ${"{{ currency }}"} `;
default:
return price.toFixed(2);
}
}
function updatePrice() {
if (domainInput.value) {
const registrarId = registrarDropdown.value;
getDomainPrice(domainInput.value, yearInput.value, registrarId).then(price => {
priceValue.innerText = formatPrice(price);
const domainValue = document.getElementById('domainName')?.value.trim() || "";
const registrarId = document.getElementById('registrarDropdown')?.value || "";
const years = parseInt(document.getElementById('transferYears')?.value, 10) || 1;
if (domainValue) {
getDomainPrice(domainValue, years, registrarId).then(({ price, type }) => {
if (isNaN(price)) {
console.error("Invalid price received:", price);
priceValue.innerText = formatPrice(0.00);
return;
}
// Multiply price by years
const totalPrice = price * years;
priceValue.innerText = formatPrice(totalPrice);
priceDisplay.style.display = 'block';
// Remove existing color classes
priceValue.classList.remove('text-red', 'text-green', 'text-blue');
// Apply appropriate colors based on type
if (type === "promotion") {
priceValue.classList.add('text-green'); // Mark as promotion
priceDisplay.title = "Promotional Price";
} else if (type === "premium") {
priceValue.classList.add('text-red'); // Mark as premium
priceDisplay.title = "Premium Price";
} else {
priceValue.classList.add('text-blue'); // Default regular price
priceDisplay.title = "Regular Price";
}
}).catch(error => {
console.error("Error fetching price:", error);
priceDisplay.style.display = 'none';
});
} else {
priceDisplay.style.display = 'none';
@ -202,6 +191,7 @@ document.addEventListener("DOMContentLoaded", function() {
domainInput.addEventListener('input', updatePrice);
yearInput.addEventListener('input', updatePrice);
registrarDropdown.addEventListener('change', updatePrice);
yearSlider.addEventListener('input', updatePrice);
});
</script>
{% endblock %}

View file

@ -163,6 +163,7 @@ $app->group('', function ($route) {
$route->get('/dapi/applications', [DapiController::class, 'listApplications']);
$route->get('/dapi/payments', [DapiController::class, 'listPayments']);
$route->get('/dapi/statements', [DapiController::class, 'listStatements']);
$route->get('/dapi/domain/price', [DapiController::class, 'domainPrice']);
})->add(new AuthMiddleware($container));
$app->any('/api[/{params:.*}]', function (