Skip to content

Commit c1e27a6

Browse files
dgjlindsayclaude
andcommitted
ABN-298/feat: extended net terms, surcharge configuration, and dev tooling
Extended net terms with configurable surcharge strategy (fixed, percentage, or both). Per-term surcharge grid with differential mode. Dev tooling: FRP proxy, Xdebug, debug mode, proxy auto-start in install target. Fixes: surcharge field visibility on "Use System Value" toggle, field value reset on inherit re-check, proxy base_link_url in patch-proxy section 3, broken validation rule blocking admin config save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bb83fbb commit c1e27a6

40 files changed

Lines changed: 3212 additions & 148 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ composer.lock
1919

2020
# Local dev config
2121
.env.local
22+
.frpc.pid
2223

2324
# Python
2425
*.venv
26+
.phpunit.result.cache

Api/Config/RepositoryInterface.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,21 @@ interface RepositoryInterface
4040
public const XML_PATH_ENABLE_PO_NUMBER = 'payment/two_payment/enable_po_number';
4141
public const XML_PATH_PAYMENT_TERMS_TYPE = 'payment/two_payment/payment_terms_type';
4242
public const XML_PATH_PAYMENT_TERMS_DURATION_DAYS = 'payment/two_payment/payment_terms_duration_days';
43+
public const XML_PATH_PAYMENT_TERMS = 'payment/two_payment/payment_terms';
44+
public const XML_PATH_DEFAULT_PAYMENT_TERM = 'payment/two_payment/default_payment_term';
45+
public const XML_PATH_SURCHARGE_TYPE = 'payment/two_payment/surcharge_type';
46+
public const XML_PATH_SURCHARGE_DIFFERENTIAL = 'payment/two_payment/surcharge_differential';
47+
public const XML_PATH_SURCHARGE_LINE_DESCRIPTION = 'payment/two_payment/surcharge_line_description';
48+
public const XML_PATH_SURCHARGE_TAX_RATE = 'payment/two_payment/surcharge_tax_rate';
49+
public const XML_PATH_DEFAULT_PRODUCT_TAX_CLASS = 'tax/classes/default_product_tax_class';
4350
public const XML_PATH_VERSION = 'payment/two_payment/version';
4451
public const XML_PATH_DEBUG = 'payment/two_payment/debug';
4552

53+
/** Configurable limits — override in fork */
54+
public const AVAILABLE_PAYMENT_TERMS = [14, 30, 60, 90];
55+
public const SURCHARGE_FIXED_MAX = 100;
56+
public const SURCHARGE_PERCENTAGE_MAX = 100;
57+
4658
/** Weight unit */
4759
public const XML_PATH_WEIGHT_UNIT = 'general/locale/weight_unit';
4860

@@ -263,11 +275,84 @@ public function isAddressSearchEnabled(?int $storeId = null): bool;
263275
public function getPaymentTermsType(?int $storeId = null): string;
264276

265277
/**
266-
* Get payment terms duration days
278+
* Get payment terms duration days (custom term, 0 = not set)
267279
*
268280
* @param int|null $storeId
269281
*
270282
* @return int
271283
*/
272284
public function getPaymentTermsDurationDays(?int $storeId = null): int;
285+
286+
/**
287+
* Get selected payment terms from multiselect
288+
*
289+
* @param int|null $storeId
290+
*
291+
* @return array
292+
*/
293+
public function getPaymentTerms(?int $storeId = null): array;
294+
295+
/**
296+
* Get all buyer-facing terms (union of multiselect + custom duration)
297+
*
298+
* @param int|null $storeId
299+
*
300+
* @return array
301+
*/
302+
public function getAllBuyerTerms(?int $storeId = null): array;
303+
304+
/**
305+
* Get default payment term
306+
*
307+
* @param int|null $storeId
308+
*
309+
* @return int
310+
*/
311+
public function getDefaultPaymentTerm(?int $storeId = null): int;
312+
313+
/**
314+
* Get surcharge type
315+
*
316+
* @param int|null $storeId
317+
*
318+
* @return string
319+
*/
320+
public function getSurchargeType(?int $storeId = null): string;
321+
322+
/**
323+
* Check if differential surcharge is enabled
324+
*
325+
* @param int|null $storeId
326+
*
327+
* @return bool
328+
*/
329+
public function isSurchargeDifferential(?int $storeId = null): bool;
330+
331+
/**
332+
* Get surcharge line item description
333+
*
334+
* @param int|null $storeId
335+
*
336+
* @return string
337+
*/
338+
public function getSurchargeLineDescription(?int $storeId = null): string;
339+
340+
/**
341+
* Get surcharge tax rate (percentage)
342+
*
343+
* @param int|null $storeId
344+
*
345+
* @return float
346+
*/
347+
public function getSurchargeTaxRate(?int $storeId = null): float;
348+
349+
/**
350+
* Get surcharge config for a specific term
351+
*
352+
* @param int $days
353+
* @param int|null $storeId
354+
*
355+
* @return array{percentage: int, fixed: int, limit: float}
356+
*/
357+
public function getSurchargeConfig(int $days, ?int $storeId = null): array;
273358
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
/**
3+
* Copyright © Two.inc All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Two\Gateway\Block\Adminhtml\System\Config\Field;
9+
10+
use Magento\Backend\Block\Template\Context;
11+
use Magento\Config\Block\System\Config\Form\Field;
12+
use Magento\Framework\App\Config\ScopeConfigInterface;
13+
use Magento\Framework\Data\Form\Element\AbstractElement;
14+
use Two\Gateway\Api\Config\RepositoryInterface as ConfigRepository;
15+
16+
/**
17+
* Renders a grid of surcharge inputs (fixed, percentage, limit) per payment term.
18+
*
19+
* Replaces the individual per-term surcharge fields with a compact table.
20+
* Reads available terms from the multiselect + custom duration config,
21+
* and the limits from RepositoryInterface constants (fork-friendly).
22+
*/
23+
class SurchargeGrid extends Field
24+
{
25+
/** @var string */
26+
protected $_template = 'Two_Gateway::system/config/field/surcharge-grid.phtml';
27+
28+
/** @var ScopeConfigInterface */
29+
private $scopeConfig;
30+
31+
/** @var string */
32+
private $scope = 'default';
33+
34+
/** @var int */
35+
private $scopeId = 0;
36+
37+
public function __construct(
38+
Context $context,
39+
ScopeConfigInterface $scopeConfig,
40+
array $data = []
41+
) {
42+
parent::__construct($context, $data);
43+
$this->scopeConfig = $scopeConfig;
44+
}
45+
46+
/**
47+
* @inheritDoc
48+
*/
49+
public function render(AbstractElement $element): string
50+
{
51+
$this->resolveScope($element);
52+
$element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue();
53+
return parent::render($element);
54+
}
55+
56+
/**
57+
* @inheritDoc
58+
*/
59+
protected function _getElementHtml(AbstractElement $element): string
60+
{
61+
return $this->_toHtml();
62+
}
63+
64+
/**
65+
* Get the sorted list of active payment terms (standard + custom).
66+
*/
67+
public function getActiveTerms(): array
68+
{
69+
$selected = $this->getConfigValue(ConfigRepository::XML_PATH_PAYMENT_TERMS);
70+
$terms = array_filter(array_map('intval', explode(',', (string)$selected)));
71+
72+
$custom = (int)$this->getConfigValue(ConfigRepository::XML_PATH_PAYMENT_TERMS_DURATION_DAYS);
73+
if ($custom > 0) {
74+
$terms[] = $custom;
75+
}
76+
77+
$terms = array_unique($terms);
78+
sort($terms);
79+
return array_values($terms);
80+
}
81+
82+
/**
83+
* Get the saved surcharge value for a given term and field.
84+
*/
85+
public function getSavedValue(int $days, string $field): string
86+
{
87+
$path = sprintf('payment/two_payment/surcharge_%d_%s', $days, $field);
88+
$value = $this->getConfigValue($path);
89+
return $value !== null ? (string)$value : '';
90+
}
91+
92+
/**
93+
* Get the default payment term (for differential mode highlighting).
94+
*/
95+
public function getDefaultTerm(): int
96+
{
97+
return (int)$this->getConfigValue(ConfigRepository::XML_PATH_DEFAULT_PAYMENT_TERM);
98+
}
99+
100+
/**
101+
* Get the surcharge type (none, percentage, fixed, fixed_and_percentage).
102+
*/
103+
public function getSurchargeType(): string
104+
{
105+
return (string)$this->getConfigValue(ConfigRepository::XML_PATH_SURCHARGE_TYPE);
106+
}
107+
108+
public function getMaxFixed(): int
109+
{
110+
return ConfigRepository::SURCHARGE_FIXED_MAX;
111+
}
112+
113+
public function getMaxPercentage(): int
114+
{
115+
return ConfigRepository::SURCHARGE_PERCENTAGE_MAX;
116+
}
117+
118+
/**
119+
* Get the HTML field name for a surcharge input.
120+
*
121+
* Nests under the surcharge_grid field's value so the backend model receives it:
122+
* groups[payment_terms][fields][surcharge_grid][value][{days}][{field}]
123+
*/
124+
public function getFieldName(int $days, string $field): string
125+
{
126+
return sprintf(
127+
'groups[payment_terms][fields][surcharge_grid][value][%d][%s]',
128+
$days,
129+
$field
130+
);
131+
}
132+
133+
/**
134+
* Get the "inherit" checkbox name for scope override.
135+
*/
136+
public function getInheritName(int $days, string $field): string
137+
{
138+
return sprintf(
139+
'groups[payment_terms][fields][surcharge_grid][inherit][%d][%s]',
140+
$days,
141+
$field
142+
);
143+
}
144+
145+
/**
146+
* Check if a field is using the inherited (default/website) value at current scope.
147+
*/
148+
public function isInherited(int $days, string $field): bool
149+
{
150+
if ($this->scope === 'default') {
151+
return false;
152+
}
153+
$path = sprintf('payment/two_payment/surcharge_%d_%s', $days, $field);
154+
// Check if a value exists at this specific scope
155+
$value = $this->scopeConfig->getValue($path, $this->scope, $this->scopeId);
156+
$defaultValue = $this->scopeConfig->getValue($path);
157+
// If the scope-specific value equals the default, it's likely inherited
158+
// (Magento doesn't expose "is this overridden" directly for system config)
159+
return $value === $defaultValue;
160+
}
161+
162+
/**
163+
* Whether we're at a non-default scope (website or store).
164+
*/
165+
public function isNonDefaultScope(): bool
166+
{
167+
return $this->scope !== 'default';
168+
}
169+
170+
/**
171+
* Available term constants (for JS to know which terms are standard).
172+
*/
173+
public function getAvailablePaymentTerms(): array
174+
{
175+
return ConfigRepository::AVAILABLE_PAYMENT_TERMS;
176+
}
177+
178+
private function resolveScope(AbstractElement $element): void
179+
{
180+
$form = $element->getForm();
181+
if ($form) {
182+
$scope = (string)$form->getScope();
183+
$this->scope = ($scope !== '') ? $scope : 'default';
184+
$this->scopeId = (int)$form->getScopeId();
185+
}
186+
}
187+
188+
private function getConfigValue(string $path)
189+
{
190+
if ($this->scope !== 'default') {
191+
return $this->scopeConfig->getValue($path, $this->scope, $this->scopeId);
192+
}
193+
return $this->scopeConfig->getValue($path);
194+
}
195+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Copyright © Two.inc All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Two\Gateway\Block\Adminhtml\System\Config\Field;
9+
10+
use Magento\Backend\Block\Template\Context;
11+
use Magento\Config\Block\System\Config\Form\Field;
12+
use Magento\Framework\Data\Form\Element\AbstractElement;
13+
use Two\Gateway\Api\Config\RepositoryInterface as ConfigRepository;
14+
15+
/**
16+
* Surcharge Tax Rate field with dynamic comment.
17+
*
18+
* Shows the store's default tax rate when available, or a red warning
19+
* when no tax rules are configured.
20+
*/
21+
class SurchargeTaxRate extends Field
22+
{
23+
/**
24+
* @var ConfigRepository
25+
*/
26+
private $configRepository;
27+
28+
public function __construct(
29+
Context $context,
30+
ConfigRepository $configRepository,
31+
array $data = []
32+
) {
33+
parent::__construct($context, $data);
34+
$this->configRepository = $configRepository;
35+
}
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
protected function _getElementHtml(AbstractElement $element): string
41+
{
42+
$defaultRate = $this->configRepository->getDefaultTaxRate();
43+
44+
if ($defaultRate > 0) {
45+
$element->setComment(
46+
(string)__(
47+
'Leave empty to use your store\'s default tax rate (%1%%). Enter 0 for tax-exempt.',
48+
number_format($defaultRate, 1)
49+
)
50+
);
51+
} else {
52+
$element->setComment(
53+
'<span class="surcharge-tax-warning">'
54+
. (string)__('Warning: No tax rules are configured for your store. '
55+
. 'Configure tax rules in Stores → Tax Rules to ensure correct surcharge tax calculation. '
56+
. 'Enter a rate manually, or enter 0 for tax-exempt.')
57+
. '</span>'
58+
);
59+
}
60+
61+
return parent::_getElementHtml($element);
62+
}
63+
}

0 commit comments

Comments
 (0)