Skip to content

IMP-424: Extended net terms and surcharge configuration#78

Open
dgjlindsay wants to merge 7 commits intomainfrom
doug/ABN-xxx-Magento-admin-controls-for-extended-net-terms
Open

IMP-424: Extended net terms and surcharge configuration#78
dgjlindsay wants to merge 7 commits intomainfrom
doug/ABN-xxx-Magento-admin-controls-for-extended-net-terms

Conversation

@dgjlindsay
Copy link
Copy Markdown
Contributor

Summary

  • Multi-term selection: Merchants select from configurable payment terms (14/30/60/90 days for Two, 30/60/90 for ABN fork) plus an optional custom duration field
  • Surcharge configuration: Per-term percentage, fixed fee, and limit fields with four surcharge modes (none/percentage/fixed/both). Differential surcharge option only charges for term extensions beyond the default
  • Surcharge calculator: SurchargeCalculator service calls POST /pricing/v1/portal/order/fee to compute buyer fees, with differential mode making two API calls
  • Fork-friendly constants: AVAILABLE_PAYMENT_TERMS, SURCHARGE_FIXED_MAX, SURCHARGE_PERCENTAGE_MAX in RepositoryInterface for easy override
  • Admin JS: Four-layer dependency management (term selection → default term options → surcharge visibility → differential field disabling)
  • Translations: All new strings in nl_NL, nb_NO, sv_SE

Branding notes for fork

Base plugin (Two) Fork (ABN Amro)
"the fee Two charges" "the fee ABN AMRO charges"
AVAILABLE_PAYMENT_TERMS: [14,30,60,90] [30,60,90]
SURCHARGE_FIXED_MAX: 100 25
Default surcharge description: "Payment terms fee" "Betaaltermijn toeslag"

Test plan

  • make test — 60 unit tests pass (verified)
  • Deploy to staging, verify admin panel shows new Payment Terms group
  • Select multiple terms → verify default term dropdown updates
  • Select surcharge method → verify correct per-term fields appear
  • Enable differential → verify default term fields are disabled
  • Test backward compatibility: existing stores with payment_terms_duration_days=30 should see [30] as buyer terms
  • Verify translations render in NL/NO/SE admin panels

🤖 Generated with Claude Code

Admin config for multi-term selection (14/30/60/90 days), custom
duration, default term, surcharge method (percentage/fixed/both),
differential surcharge for term extensions, configurable line item
description and tax rate. SurchargeCalculator calls the pricing API
to compute buyer fees. ComposeOrder sends available_terms to the API
for buyer-facing checkout selection.

Configurable limits (AVAILABLE_PAYMENT_TERMS, SURCHARGE_FIXED_MAX,
SURCHARGE_PERCENTAGE_MAX) in RepositoryInterface for easy fork override.
Translations added for nl_NL, nb_NO, sv_SE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the payment terms and surcharge capabilities of the system, providing merchants with greater flexibility in configuring payment options for their customers. It introduces a robust framework for managing multiple payment durations and applying various types of surcharges, including a differential mode that intelligently adjusts fees based on term extensions. The changes are supported by a new backend service for surcharge calculation, an updated admin interface for intuitive configuration, and a comprehensive suite of tests to ensure stability and correctness.

Highlights

  • Extended Payment Terms Configuration: Introduced multi-term selection for payment terms, allowing merchants to offer various durations (e.g., 14, 30, 60, 90 days) and an optional custom duration field.
  • Comprehensive Surcharge Configuration: Implemented a robust surcharge configuration system, enabling per-term percentage, fixed fees, and limits, with four distinct surcharge modes (none, percentage, fixed, both).
  • Differential Surcharge Option: Added a differential surcharge option, which applies surcharges only for term extensions beyond the default, providing flexibility for buyers.
  • Surcharge Calculator Service: Developed a new SurchargeCalculator service to accurately compute buyer fees by interacting with the /pricing/v1/portal/order/fee API endpoint, supporting differential mode with multiple API calls.
  • Enhanced Admin Panel UI: Improved the admin panel with dynamic JavaScript to manage dependencies between term selection, default term options, surcharge visibility, and differential field disabling, enhancing user experience.
  • Fork-Friendly Constants: Provided configurable constants (AVAILABLE_PAYMENT_TERMS, SURCHARGE_FIXED_MAX, SURCHARGE_PERCENTAGE_MAX) in RepositoryInterface for easy customization in different environments or forks.
  • Improved API Error Handling: Enhanced API error handling to provide more specific and user-friendly messages, distinguishing between validation and system errors, and cleaning up Pydantic-specific error message prefixes/suffixes.
  • Expanded Testing Infrastructure: Added extensive unit and end-to-end testing infrastructure, including new PHPUnit test suites and Magento class stubs, to ensure the reliability and maintainability of the new features.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/phpunit.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive functionality for managing payment terms and surcharges within the system. Key changes include extending the configuration interface and repository to support multiple payment terms, custom durations, default terms, and various surcharge types (percentage, fixed, differential, with tax rates). A new SurchargeCalculator service has been added to determine buyer surcharges based on these configurations and API fees. The admin UI has been updated with new configuration fields and dynamic JavaScript logic for managing payment term and surcharge settings. Additionally, the pull request enhances API error handling by providing more specific and user-friendly messages for validation and system errors, and significantly expands the testing suite with new unit and end-to-end tests, along with necessary Magento stubs and a PHPUnit bootstrap. The review feedback suggests improving the abstraction of the RepositoryInterface by using methods instead of constants for data arrays, replacing a magic number with a named constant in getDefaultPaymentTerm, and dynamically passing the TERM_DAYS array to JavaScript from the backend to prevent hardcoding inconsistencies.

Comment on lines +54 to +56
public const AVAILABLE_PAYMENT_TERMS = [14, 30, 60, 90];
public const SURCHARGE_FIXED_MAX = 100;
public const SURCHARGE_PERCENTAGE_MAX = 100;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defining data arrays like AVAILABLE_PAYMENT_TERMS, SURCHARGE_FIXED_MAX, and SURCHARGE_PERCENTAGE_MAX directly as constants in an interface can make the interface less abstract. While the comment "override in fork" suggests this is the intended mechanism for customization, a more flexible approach might be to define methods that return these values, allowing implementing classes to provide their own logic or data sources. This would keep the interface purely behavioral.

return $default;
}
$terms = $this->getAllBuyerTerms($storeId);
return $terms ? min($terms) : 30;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback value 30 in getDefaultPaymentTerm is a magic number. It would be more maintainable and readable to define this as a named constant, perhaps DEFAULT_PAYMENT_TERM_DAYS, in the RepositoryInterface or Repository class. This improves clarity and makes it easier to change if the default ever needs to be adjusted.

        return $terms ? min($terms) : self::DEFAULT_PAYMENT_TERM_DAYS;

'use strict';

// Payment Terms Configuration Handler
var TERM_DAYS = [14, 30, 60, 90];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The TERM_DAYS array is hardcoded here, mirroring RepositoryInterface::AVAILABLE_PAYMENT_TERMS. If the PHP constant is updated, this JavaScript array would also need manual synchronization. To prevent potential inconsistencies and reduce maintenance effort, consider passing this array dynamically from the backend (e.g., via window.config or a dedicated API endpoint) rather than hardcoding it in the frontend JavaScript.

Comment on lines +390 to +397
public function getDefaultPaymentTerm(?int $storeId = null): int
{
$default = (int)$this->getConfig(self::XML_PATH_DEFAULT_PAYMENT_TERM, $storeId);
if ($default > 0) {
return $default;
}
$terms = $this->getAllBuyerTerms($storeId);
return $terms ? min($terms) : 30;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDefaultPaymentTerm() does not validate that the stored value is actually in the enabled terms set. If a merchant selects e.g. 14 days as default but has not enabled 14-day terms in the multiselect, this method returns 14. ComposeOrder then sends terms.duration_days = 14 to the API while available_terms (built from getAllBuyerTerms) omits 14 — the default term is absent from its own available-terms list, which could cause order-creation failures.

Suggested change
public function getDefaultPaymentTerm(?int $storeId = null): int
{
$default = (int)$this->getConfig(self::XML_PATH_DEFAULT_PAYMENT_TERM, $storeId);
if ($default > 0) {
return $default;
}
$terms = $this->getAllBuyerTerms($storeId);
return $terms ? min($terms) : 30;
public function getDefaultPaymentTerm(?int $storeId = null): int
{
$default = (int)$this->getConfig(self::XML_PATH_DEFAULT_PAYMENT_TERM, $storeId);
$terms = $this->getAllBuyerTerms($storeId);
if ($default > 0 && in_array($default, $terms, true)) {
return $default;
}
return $terms ? min($terms) : 30;
}

The root cause is that the default_payment_term admin dropdown uses AvailablePaymentTerms as its source model (listing all 14/30/60/90 options) rather than a dynamic model reflecting only the currently-enabled terms, so the guard in getDefaultPaymentTerm() is needed regardless.

dgjlindsay and others added 4 commits March 26, 2026 13:13
Checkout integration:
- Buyer-facing term selector dropdown (hidden when only one term available)
- selectedTerm flows through DataAssignObserver → ComposeOrder
- SurchargeCalculator injected into ComposeOrder via constructor
- Surcharge added as line item (type: SURCHARGE) with tax calculation
- Order totals (gross/net/tax) adjusted to include surcharge
- ConfigProvider passes availableBuyerTerms + defaultPaymentTerm to frontend

Tests (35 new, 95 total):
- RepositoryPaymentTermsTest: 22 tests covering getPaymentTerms,
  getAllBuyerTerms, getDefaultPaymentTerm, getSurchargeType/Config/TaxRate
- SurchargeCalculatorTest: 13 tests covering percentage, fixed, combined,
  limit cap, cent rounding, differential mode, end-of-month, tax+description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hen unconfigured

Replace the hidden surcharge_show_tax_rate toggle with auto-detection:
- Empty surcharge_tax_rate field falls back to Magento's default tax
  rate via TaxCalculationInterface::getDefaultCalculatedRate()
- Explicit "0" means tax-exempt (merchant override)
- New SurchargeTaxRate frontend_model Block shows dynamic comment:
  green "defaults to X%" when tax rules exist, red warning when not
- Remove surcharge_show_tax_rate from system.xml, config.xml, and
  RepositoryInterface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six tests covering the POST /pricing/v1/portal/order/fee endpoint:
valid 30/90-day fees, longer-terms-cost-more assertion, end-of-month
terms, invalid country code, and zero-duration error handling.

Tests skip gracefully with a clear message when the API key lacks
pricing endpoint permissions, keeping the suite green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the latest commits. The critical issue flagged in the previous review — getDefaultPaymentTerm() returning a configured value that may not be present in getAllBuyerTerms() — is still unresolved. See the existing inline comment with a suggested fix. No new critical issues found in the updated diff.

dgjlindsay and others added 2 commits March 27, 2026 20:05
…lation

Switch from portal-only /pricing/v1/portal/order/fee to the new
API-key-authed /v1/pricing/order/fee endpoint. Fix RealCurl header/option
accumulation across sequential requests in E2E tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Named arguments are a PHP 8.0 feature. CI runs against PHP 7.4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after today's update. The critical issue flagged in the previous review remains unresolved: getDefaultPaymentTerm() in Model/Config/Repository.php does not validate that the stored default value is present in the enabled terms set (see existing inline comment with suggested fix). No new critical issues found in the additional commits (SurchargeCalculator, ComposeOrder integration, test infrastructure). This PR should not be merged until the getDefaultPaymentTerm() guard is applied.

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the force-push. The critical bug flagged in the earlier thread (PRRT_kwDOHkfW4s53AhoJ) is still present: getDefaultPaymentTerm() returns a stored value without checking it exists in getAllBuyerTerms(), meaning terms.duration_days sent to the API can be absent from available_terms. The suggestion block in that thread shows the fix. Please address before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant