Skip to content

ABN-298 / ABN-356 / ABN-366: chip-summary sync, fee column polish, deferred invoice#83

Open
dgjlindsay wants to merge 6 commits intomainfrom
doug/ABN-298-chip-summary-mismatch
Open

ABN-298 / ABN-356 / ABN-366: chip-summary sync, fee column polish, deferred invoice#83
dgjlindsay wants to merge 6 commits intomainfrom
doug/ABN-298-chip-summary-mismatch

Conversation

@dgjlindsay
Copy link
Copy Markdown
Contributor

Summary

Five commits, four tickets, one branch.

  • ABN-298 — pass storeId from Model/Ui/ConfigProvider into SurchargeCalculator::calculate() so chip values resolve at the same scope as the Total Collector segment. Without this, any merchant config set at website/store scope (typical) made the page-load chip labels diverge from the order-summary segment value. Matches the "sometimes, mysteriously, the chips would show a different fee to the order summary" symptom.
  • ABN-356 — three changes to the admin surcharge grid Fee column:
    1. Omit zero components: "x%" if fixed is 0, "y" if percentage is 0, "0.00" if both.
    2. Refetch fees on any term-set change (term checkbox / custom-days / inherit toggle). Memoised by joined-terms key inside loadFees() so non-term-set changes collapse to no-op; .fail clears the memo so transient errors are retryable.
    3. Animated three-dot loading placeholder (CSS keyframes) replacing the static em-dash while a fetch is in flight; em-dash returns when the server omits a row.
  • ABN-366 — drop the eager CAPTURE_ONLINE invoice creation from OrderService::processOrder(). Magento invoice now lives behind the actual fulfilment in both observers (SalesOrderShipmentAfter for trigger=shipment, SalesOrderSaveAfter for trigger=complete), created with CAPTURE_OFFLINE after the /v1/order/{id}/fulfillments POST succeeds — so Two::capture() doesn't double-post. Idempotency on the complete branch via \$order->hasInvoices(). Shipment observer's elseif (trigger == 'complete') removed; SaveAfter is the sole trigger=complete handler.

Test plan

  • Place an order with fulfill_trigger=shipment: confirm shows Processing, no invoice, cancel button visible. Ship → invoice created and paid, email sends.
  • Place an order with fulfill_trigger=complete: confirm shows Processing, no invoice. Save with status in fulfill_order_status list → /fulfillments posts, invoice created and paid. Re-save: no second /fulfillments (gated by hasInvoices()).
  • fulfill_trigger=invoice (admin clicks Invoice in UI): unchanged path.
  • Configure surcharge at website scope, open checkout: chip label matches order-summary segment value.
  • Surcharge config grid: tick a new term → loading dots animate → fee renders within one round-trip. Type a custom-days value → fee renders for the typed value.
  • Force a 500 on the fees proxy: cells keep prior content, next term-change retries.
  • Surcharge config with one component zero: cell shows only the non-zero side.

🤖 Generated with Claude Code

dgjlindsay and others added 5 commits April 25, 2026 15:20
Total Collector passes storeId to SurchargeCalculator::calculate() so
surcharge config resolves at the quote's store scope. ConfigProvider
omitted the arg, falling through to default scope. Any merchant config
set at website/store scope (typical) made the page-load chip values
diverge from the segment value computed by the Total Collector — chip
labels disagreed with the order summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cell text was always "x% + y" even when one side was zero. Now only
non-zero components render: pct alone if fixed is zero, fixed alone if
pct is zero, "0.00" if both are zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
processOrder() previously created and registered a Magento invoice via
CAPTURE_ONLINE at order-confirmation time when fulfill_trigger was set
to 'shipment' or 'complete'. Result: order showed up under Sales →
Invoices immediately, the cancel button on Sales → Orders was hidden,
and the invoice email fired before Two had actually invoiced the buyer.

Drop the eager invoice creation. processOrder() now just promotes the
order to Processing and records the authorisation transaction.

Move invoice creation to the two observers that already post to Two's
/fulfillments endpoint:

- SalesOrderShipmentAfter: after a successful /fulfillments POST for
  the whole order, prepareInvoice → CAPTURE_OFFLINE → register → pay.
  CAPTURE_OFFLINE is critical — CAPTURE_ONLINE would route through
  Two::capture() and re-post /fulfillments.
  The trigger='complete' branch is removed; SalesOrderSaveAfter is the
  sole handler for that trigger.

- SalesOrderSaveAfter: after a successful /fulfillments POST, create
  the invoice with the same CAPTURE_OFFLINE shape. Idempotency gated
  via $order->hasInvoices() — once we've fulfilled, subsequent saves
  of the same order return early before re-posting to Two.

Two::canCapture() / Two::capture() unchanged: trigger='invoice' still
flows admin-clicks-Invoice → Magento::capture() → /fulfillments →
register, which was already correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
loadFees() was only called once at module init. Toggling a term
checkbox or typing a new value into the custom-days input updated the
grid rows but left new cells stuck at "—" until a full page reload.

Wire loadFees() into update() so any term-set mutation triggers a
refetch. Memoise the joined terms key inside loadFees() to collapse
repeat fires from update() (every change handler hits update(), most
of them don't actually change the terms set). On AJAX failure the memo
is cleared so a transient error is retryable on the next event; cells
keep whatever they last showed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the static em-dash placeholder with three pulsing dots so the
merchant has visual feedback while the proxy fetches per-term fees from
Two. Dots are pure CSS (keyframed opacity + scale, staggered delays)
and live in initial server-rendered cells, dynamically created rows,
and any cell awaiting its first response. When the response lands,
cells that have a fee render the value; cells the server omitted fall
back to the em-dash so the loading state ends cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread Observer/SalesOrderShipmentAfter.php Outdated
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 refactors the order fulfillment process by centralizing invoice creation through a new createOfflinePaidInvoice method in the order save and shipment observers. It also simplifies the OrderService by removing redundant invoicing logic and enhances the admin surcharge grid with loading indicators and improved fee calculation. Feedback was provided regarding the handling of partial shipments for invoicing, the use of deprecated transaction and save methods in favor of repositories, and inconsistent naming constants for branding.

$invoice->pay();
$invoice->setTransactionId($order->getPayment()->getLastTransId());
$invoice->save();
$this->createOfflinePaidInvoice($order);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation only creates a Magento invoice when the entire order is shipped. If the fulfill_trigger is set to shipment, Magento should ideally create a partial invoice corresponding to the items in the current shipment, especially since a partial fulfillment request was just sent to the Two API (line 119). Consider removing the isWholeOrderShipped gate and passing the shipment's quantities to prepareInvoice to support partial invoicing.

                $this->createOfflinePaidInvoice($order, $shipment);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Opus 4.7] Out of scope for ABN-366 per the implementation brief: "For partial shipments: keep the current pass-through behaviour (we have no ask to support partial Magento invoicing yet)." Two-side partial fulfilments still post correctly via line 119; only the Magento-side partial invoice is deferred. Will revisit when there is a concrete merchant ask.

Comment thread Observer/SalesOrderSaveAfter.php Outdated
if (!$this->isWholeOrderShipped($order)) {
$error = __(
"%1 requires whole order to be shipped before it can be fulfilled.",
$this->configRepository::PRODUCT_NAME
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 constant PRODUCT_NAME is used here, but PROVIDER is still used in other parts of the file (lines 200 and 205). For consistency and to ensure the correct branding is displayed to the merchant, please use PRODUCT_NAME for all status history comments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Opus 4.7] Resolved the other way in 1253c42 — reverted my new PRODUCT_NAME usages to PROVIDER, which is the existing repo-wide convention here (10 files use PROVIDER, 0 used PRODUCT_NAME before this PR). PRODUCT_NAME came in via a port brief from the ABN fork where the constant is named differently; both constants are equal strings (Two) so no functional change either way.

Comment on lines +152 to +155
$this->transaction
->addObject($invoice)
->addObject($invoice->getOrder())
->save();
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 use of Magento\Framework\DB\Transaction and the save() method on models is deprecated in Magento 2. It is recommended to use Magento\Sales\Api\InvoiceRepositoryInterface and Magento\Sales\Api\OrderRepositoryInterface to persist these entities. Additionally, Transaction should be replaced with Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface if atomic operations across multiple resource models are required.

        $this->invoiceRepository->save($invoice);
        $this->orderRepository->save($invoice->getOrder());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Opus 4.7] Keeping \Magento\Framework\DB\Transaction for atomicity across the invoice + order saves — switching to InvoiceRepositoryInterface::save + OrderRepositoryInterface::save would split into two separate transactions, with no rollback if the second fails after the first commits. The Transaction wrapper is the documented Magento 2 pattern for paired entity saves and is used elsewhere in this repo (e.g. it is the same shape OrderService::processOrder previously used for the eager-invoice path). No deprecation notice on the class itself in current Magento 2.4.x.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yh makes more sense to have atomicity across the invoice + order saves as a grouped action rather than on a per save basis.

- Idempotency guard on shipment branch: skip invoice creation if the
  order already has invoices, mirroring the SalesOrderSaveAfter check.
  Without this, a second sales_order_shipment_save_after for the same
  fully-shipped order would call prepareInvoice() on an already-fully-
  invoiced order and throw "Cannot create an invoice without products."

- Revert PRODUCT_NAME usages back to PROVIDER in three files. The repo
  convention here is PROVIDER (10 files); PRODUCT_NAME was carried over
  from the ABN-fork brief where the constant is named differently.
  Restoring consistency within this fork's codebase.

- Pick up an admin-grid header padding tweak that was already on disk.

Co-Authored-By: Claude Opus 4.7 (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.

The idempotency guard flagged in my earlier review (!$order->hasInvoices() in SalesOrderShipmentAfter) has been applied in 1253c42. Resolving that thread.

Re-reviewed the full diff at this commit — no critical issues. The deferred-invoice refactor, storeId scope fix, and fee-column polish all look correct. Ready for human approval.

Comment on lines +152 to +155
$this->transaction
->addObject($invoice)
->addObject($invoice->getOrder())
->save();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yh makes more sense to have atomicity across the invoice + order saves as a grouped action rather than on a per save basis.

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.

2 participants