Skip to content

Commit ccd87b8

Browse files
committed
feat: persist TLD sync pricing settings
1 parent 2e31ab4 commit ccd87b8

4 files changed

Lines changed: 283 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Synergy Wholesale WHMCS Domains Module
44

55
## Unreleased Version [Updated xx/xx/20xx]
66
### Added
7-
-
7+
- Persistent margin and rounding settings for Synergy Wholesale TLD pricing sync.
8+
- Configurable minimum renew and transfer costs with sub-cent precision, applied before the TLD sync margin and rounding. Zero prices remain unchanged.
89

910
### Changed
1011
-

modules/registrars/synergywholesaledomains/hooks.php

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,112 @@
77
* @license https://github.com/synergywholesale/whmcs-domains-module/LICENSE
88
*/
99

10-
1110
use WHMCS\Domain\Domain;
1211
use WHMCS\View\Menu\Item as MenuItem; // http://docs.whmcs.com/Editing_Client_Area_Menus
1312
use Illuminate\Database\Capsule\Manager as Capsule;
1413

14+
require_once __DIR__ . '/synergywholesaledomains.php';
15+
16+
add_hook('AdminAreaFooterOutput', 1, function (array $vars) {
17+
if (!synergywholesaledomains_isTldSyncPage()) {
18+
return '';
19+
}
20+
21+
try {
22+
$settings = synergywholesaledomains_getTldSyncSettings();
23+
} catch (\Exception $e) {
24+
logModuleCall(SW_MODULE_NAME, 'tldSyncSettings', 'Update DB', $e->getMessage());
25+
return '';
26+
}
27+
28+
$settingsJson = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
29+
return <<<HTML
30+
<script>
31+
(function () {
32+
'use strict';
33+
34+
var saved = {$settingsJson};
35+
36+
function restore(control, value) {
37+
if (!control || value === null) {
38+
return;
39+
}
40+
if (control.tagName === 'SELECT' && !Array.prototype.some.call(control.options, function (option) {
41+
return option.value === value;
42+
})) {
43+
return;
44+
}
45+
control.value = value;
46+
control.dispatchEvent(new Event('change', { bubbles: true }));
47+
}
48+
49+
function minimumField(label, id, value) {
50+
var row = document.createElement('div');
51+
row.className = 'form-group sw-tldsync-setting';
52+
row.innerHTML = '<label class="col-md-4 col-sm-6 control-label"><span></span><br><small></small></label>'
53+
+ '<div class="col-md-4 col-sm-6"><div class="input-group">'
54+
+ '<span class="input-group-addon hidden-sm">$</span>'
55+
+ '<input type="number" min="0" step="any" class="form-control input-75">'
56+
+ '<span class="input-group-addon hidden-sm"> AUD</span></div></div>';
57+
row.querySelector('label').htmlFor = id;
58+
row.querySelector('label span').textContent = label;
59+
row.querySelector('label small').textContent = 'Configured in the Synergy Wholesale registrar settings. Applied before margin and rounding; zero prices are unchanged.';
60+
row.querySelector('input').id = id;
61+
row.querySelector('input').disabled = true;
62+
row.querySelector('input').value = value || '';
63+
return row;
64+
}
65+
66+
function enhance() {
67+
if (document.querySelector('.sw-tldsync-setting')) {
68+
return;
69+
}
70+
71+
var marginType = document.getElementById('inputMarginType');
72+
var profitPercent = document.getElementById('inputMarginPercent');
73+
var profitFixed = document.getElementById('inputMarginFixed') || profitPercent;
74+
var rounding = document.getElementById('inputRoundingValue');
75+
var importButton = document.getElementById('doTldImport');
76+
if (!marginType || !profitPercent || !rounding || !importButton) {
77+
return;
78+
}
79+
80+
restore(marginType, saved.marginType);
81+
restore(profitPercent, saved.profitMargin);
82+
restore(profitFixed, saved.profitMargin);
83+
restore(rounding, saved.rounding);
84+
85+
document.querySelector('.tld-import-percentage-margin').classList.toggle(
86+
'hidden',
87+
marginType.value !== 'percentage'
88+
);
89+
document.querySelector('.tld-import-fixed-margin').classList.toggle(
90+
'hidden',
91+
marginType.value !== 'fixed'
92+
);
93+
94+
var anchor = rounding.closest('.form-group') || rounding.parentNode;
95+
var renew = minimumField(
96+
'Minimum Renew Price',
97+
'inputMinimumRenewPrice',
98+
saved.minimumRenewPrice
99+
);
100+
var transfer = minimumField(
101+
'Minimum Transfer Price',
102+
'inputMinimumTransferPrice',
103+
saved.minimumTransferPrice
104+
);
105+
anchor.parentNode.insertBefore(renew, anchor.nextSibling);
106+
anchor.parentNode.insertBefore(transfer, renew.nextSibling);
107+
}
108+
109+
new MutationObserver(enhance).observe(document.body, { childList: true, subtree: true });
110+
enhance();
111+
}());
112+
</script>
113+
HTML;
114+
});
115+
15116

16117
/**
17118
* We have our own custom ones, so remove the default;
@@ -298,4 +399,4 @@
298399
synergywholesaledomains_Sync($vars['params']);
299400

300401
}
301-
});
402+
});

modules/registrars/synergywholesaledomains/synergywholesaledomains.php

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
define('WHATS_MY_IP_URL', 'https://{{FRONTEND}}/ip');
1717
define('SW_MODULE_VERSION', '{{VERSION}}');
1818
define('SW_MODULE_NAME', 'synergywholesaledomains');
19+
define('SW_TLDSYNC_SETTING_KEYS', [
20+
'marginType' => 'SynergyWholesaleTldSyncMarginType',
21+
'profitMargin' => 'SynergyWholesaleTldSyncProfitMargin',
22+
'rounding' => 'SynergyWholesaleTldSyncRounding',
23+
]);
1924

2025
define('SW_DNS_CONFIG_TYPES', [
2126
0 => 'Inactive',
@@ -77,6 +82,102 @@ function synergywholesaledomains_helper_getNameservers(array $params)
7782
return $nameservers;
7883
}
7984

85+
/**
86+
* Apply a minimum TLD sync price while retaining disabled and free pricing.
87+
*
88+
* @param float|int|string $price
89+
* @param float|int|string $minimum
90+
* @return float|int|string
91+
*/
92+
function synergywholesaledomains_helper_applyMinimumPrice($price, $minimum)
93+
{
94+
if ($price <= 0 || $price >= $minimum) {
95+
return $price;
96+
}
97+
98+
return (string) $minimum;
99+
}
100+
101+
function synergywholesaledomains_helper_applyTldSyncMinimums($register, $renew, $transfer, $minimumRenew, $minimumTransfer)
102+
{
103+
// Preserve the upstream registration rule before applying renew-only settings.
104+
$register = $register < $renew ? $renew : $register;
105+
106+
return [
107+
$register,
108+
synergywholesaledomains_helper_applyMinimumPrice($renew, $minimumRenew),
109+
synergywholesaledomains_helper_applyMinimumPrice($transfer, $minimumTransfer),
110+
];
111+
}
112+
113+
function synergywholesaledomains_isTldSyncPage()
114+
{
115+
$path = isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '';
116+
117+
return (bool) preg_match('#/utilities/tools/tldsync/import/?$#', $path);
118+
}
119+
120+
function synergywholesaledomains_getTldSyncSettings()
121+
{
122+
$stored = Capsule::table('tblconfiguration')
123+
->whereIn('setting', array_values(SW_TLDSYNC_SETTING_KEYS))
124+
->pluck('value', 'setting');
125+
$settings = [];
126+
127+
foreach (SW_TLDSYNC_SETTING_KEYS as $name => $key) {
128+
$settings[$name] = isset($stored[$key]) ? $stored[$key] : null;
129+
}
130+
131+
return array_merge($settings, synergywholesaledomains_getMinimumPrices());
132+
}
133+
134+
function synergywholesaledomains_getMinimumPrices()
135+
{
136+
$stored = Capsule::table('tblregistrars')
137+
->where('registrar', SW_MODULE_NAME)
138+
->whereIn('setting', ['minimumRenewPrice', 'minimumTransferPrice'])
139+
->pluck('value', 'setting');
140+
141+
$prices = [];
142+
foreach (['minimumRenewPrice', 'minimumTransferPrice'] as $setting) {
143+
$value = isset($stored[$setting]) ? localAPI('DecryptPassword', ['password2' => $stored[$setting]])['password'] : 0;
144+
$prices[$setting] = is_numeric($value) && $value >= 0 ? (string) $value : '0';
145+
}
146+
147+
return $prices;
148+
}
149+
150+
function synergywholesaledomains_saveTldSyncSettings(array $settings)
151+
{
152+
foreach (SW_TLDSYNC_SETTING_KEYS as $name => $key) {
153+
Capsule::table('tblconfiguration')->updateOrInsert(
154+
['setting' => $key],
155+
['value' => $settings[$name]]
156+
);
157+
}
158+
}
159+
160+
function synergywholesaledomains_validateTldSyncSettings(array $input)
161+
{
162+
$marginType = isset($input['margin_type']) ? $input['margin_type'] : '';
163+
$profitMargin = isset($input['margin']) ? $input['margin'] : '';
164+
$rounding = isset($input['rounding_value']) ? $input['rounding_value'] : '';
165+
if (
166+
!in_array($marginType, ['fixed', 'percentage'], true)
167+
|| !is_numeric($profitMargin)
168+
|| $profitMargin < 0
169+
|| (!in_array($rounding, ['', 'none'], true) && (!is_numeric($rounding) || $rounding < 0 || $rounding > 1))
170+
) {
171+
return null;
172+
}
173+
174+
return [
175+
'marginType' => $marginType,
176+
'profitMargin' => (string) $profitMargin,
177+
'rounding' => (string) $rounding,
178+
];
179+
}
180+
80181
/**
81182
* Sends the API requests to the Synergy Wholesale API.
82183
*
@@ -291,6 +392,20 @@ function synergywholesaledomains_getConfigArray(array $params)
291392
'Size' => '1',
292393
'Description' => 'Tick if you wish to perform a renewal on any .au domains submitted for transfer that are within 90 days of expiry.',
293394
],
395+
'minimumRenewPrice' => [
396+
'FriendlyName' => 'Minimum TLD Sync Renew Price',
397+
'Type' => 'text',
398+
'Size' => '10',
399+
'Default' => '0',
400+
'Description' => 'AUD. Supports sub-cent values. Applied to the fetched cost before margin and rounding; prices of zero are unchanged.',
401+
],
402+
'minimumTransferPrice' => [
403+
'FriendlyName' => 'Minimum TLD Sync Transfer Price',
404+
'Type' => 'text',
405+
'Size' => '10',
406+
'Default' => '0',
407+
'Description' => 'AUD. Supports sub-cent values. Applied to the fetched cost before margin and rounding; prices of zero are unchanged.',
408+
],
294409
'test_api_connection' => [
295410
'FriendlyName' => 'Check API Connectivity',
296411
'Type' => 'yesno',
@@ -3032,6 +3147,10 @@ function synergywholesaledomains_GetTldPricing(array $params)
30323147
}
30333148

30343149
$results = new WHMCS\Results\ResultsList();
3150+
$syncSettings = synergywholesaledomains_validateTldSyncSettings($_POST);
3151+
if (null !== $syncSettings) {
3152+
synergywholesaledomains_saveTldSyncSettings($syncSettings);
3153+
}
30353154

30363155
foreach ($response['pricing'] as $extension) {
30373156
$tld = '.' . $extension->tld;
@@ -3058,9 +3177,13 @@ function synergywholesaledomains_GetTldPricing(array $params)
30583177
$transferPrice = 0.00;
30593178
}
30603179

3061-
if ($registerPrice < $renewPrice) {
3062-
$registerPrice = $renewPrice;
3063-
}
3180+
list($registerPrice, $renewPrice, $transferPrice) = synergywholesaledomains_helper_applyTldSyncMinimums(
3181+
$registerPrice,
3182+
$renewPrice,
3183+
$transferPrice,
3184+
isset($params['minimumRenewPrice']) ? $params['minimumRenewPrice'] : 0,
3185+
isset($params['minimumTransferPrice']) ? $params['minimumTransferPrice'] : 0
3186+
);
30643187

30653188
$results[] = (new WHMCS\Domain\TopLevel\ImportItem())
30663189
->setExtension($tld)

tests/SynergyWholesaleDomainModuleTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,56 @@ public function testCoreModuleFunctionsExist($method)
5353
{
5454
$this->assertTrue(function_exists(SW_MODULE_NAME . '_' . $method));
5555
}
56+
57+
public function testMinimumTldSyncPrices()
58+
{
59+
$this->assertSame('-1.00', synergywholesaledomains_helper_applyMinimumPrice('-1.00', '20.00'));
60+
$this->assertSame('0.00', synergywholesaledomains_helper_applyMinimumPrice('0.00', '20.00'));
61+
$this->assertSame('20.00', synergywholesaledomains_helper_applyMinimumPrice('19.99', '20.00'));
62+
$this->assertSame('20.00', synergywholesaledomains_helper_applyMinimumPrice('20.00', '20.00'));
63+
$this->assertSame('25.00', synergywholesaledomains_helper_applyMinimumPrice('25.00', '20.00'));
64+
$this->assertSame('20.005', synergywholesaledomains_helper_applyMinimumPrice('19.99', '20.005'));
65+
}
66+
67+
public function testRenewMinimumDoesNotRaiseRegistrationPrice()
68+
{
69+
$prices = synergywholesaledomains_helper_applyTldSyncMinimums('15.00', '20.00', '10.00', '38.50', '25.00');
70+
71+
$this->assertSame('20.00', $prices[0]);
72+
$this->assertSame('38.50', $prices[1]);
73+
$this->assertSame('25.00', $prices[2]);
74+
}
75+
76+
public function testMinimumPricesAreRegistrarSettings()
77+
{
78+
$configuration = synergywholesaledomains_getConfigArray([]);
79+
80+
$this->assertSame('0', $configuration['minimumRenewPrice']['Default']);
81+
$this->assertSame('0', $configuration['minimumTransferPrice']['Default']);
82+
}
83+
84+
public function testTldSyncSettingsValidation()
85+
{
86+
$input = [
87+
'margin_type' => 'percentage',
88+
'margin' => '15',
89+
'rounding_value' => '0.95',
90+
];
91+
$settings = synergywholesaledomains_validateTldSyncSettings($input);
92+
93+
$this->assertSame('15', $settings['profitMargin']);
94+
95+
$input['rounding_value'] = '1.00';
96+
$this->assertNotNull(synergywholesaledomains_validateTldSyncSettings($input));
97+
98+
$input['margin'] = '-1';
99+
$this->assertNull(synergywholesaledomains_validateTldSyncSettings($input));
100+
}
101+
102+
public function testModernTldSyncPageDetection()
103+
{
104+
$_SERVER['REQUEST_URI'] = '/admin/index.php/admin/utilities/tools/tldsync/import';
105+
106+
$this->assertTrue(synergywholesaledomains_isTldSyncPage());
107+
}
56108
}

0 commit comments

Comments
 (0)