From 87ecd27f13110d2b4e3822f3cd9c8f65d3ffe623 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 25 Feb 2026 08:32:10 -0800 Subject: [PATCH 1/4] Send TaxCloud capture on invoice pay instead of order place --- Observer/Sales/Complete.php | 9 +++++++-- README.md | 6 ++++-- etc/events.xml | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Observer/Sales/Complete.php b/Observer/Sales/Complete.php index 29e35e6..e0fd52d 100644 --- a/Observer/Sales/Complete.php +++ b/Observer/Sales/Complete.php @@ -81,9 +81,14 @@ public function execute( return; } - $this->tclogger->info('Running Observer sales_order_place_after'); + $this->tclogger->info('Running Observer sales_order_invoice_pay (capture on payment)'); - $order = $observer->getEvent()->getOrder(); + $order = $observer->getEvent()->getInvoice()->getOrder(); + + // Only send to TaxCloud on first invoice pay to avoid duplicate API calls for partial invoices + if ($order->getInvoiceCollection()->getSize() > 1) { + return; + } $this->tcapi->authorizeCapture($order); } diff --git a/README.md b/README.md index bdb398e..a3ba6a7 100644 --- a/README.md +++ b/README.md @@ -237,14 +237,16 @@ In certain cases, a store owner may need to extend this module. Specific use cas Each of these situations can be accomplished using an event observer. For every API call to TaxCloud, this module emits a before and after event. The before events can be used to modify the parameters sent to TaxCloud's API, and the after events can be used to modify the response. +**Capture on payment:** Orders are sent to TaxCloud when the order is **captured** (first invoice paid), not when the order is placed. Canceled orders never reach TaxCloud, avoiding the need for void/refund workarounds (TaxCloud has no native "Canceled" state). This aligns with a capture-on-payment/fulfillment flow used on other platforms. + | Event Name | Description | Data Objects | | ----- | ---- | --- | | `taxcloud_lookup_before` | Emitted before the `Lookup` call to get tax rates | `$params`, `$customer`, `$address`, `$quote`, `$itemsByType`, `$shippingAssignment` | | `taxcloud_lookup_after` | Emitted after the `Lookup` call to get tax rates | `$result`, `$customer`, `$address`, `$quote`, `$itemsByType`, `$shippingAssignment` | | `taxcloud_verify_address_before` | Emitted before the `VerifyAddress` call during checkout | `$params` | | `taxcloud_verify_address_after` | Emitted after the `VerifyAddress` call during checkout | `$result` | -| `taxcloud_authorized_with_capture_before` | Emitted before the `AuthorizedWithCapture` call when an order is placed | `$params`, `$order` | -| `taxcloud_authorized_with_capture_after` | Emitted after the `AuthorizedWithCapture` call when an order is placed | `$result`, `$order` | +| `taxcloud_authorized_with_capture_before` | Emitted before the `AuthorizedWithCapture` call (when the order is sent per "Capture in TaxCloud" setting) | `$params`, `$order` | +| `taxcloud_authorized_with_capture_after` | Emitted after the `AuthorizedWithCapture` call (when the order is sent per "Capture in TaxCloud" setting) | `$result`, `$order` | | `taxcloud_returned_before` | Emitted before the `Returned` call when a credit memo is created or when a canceled unpaid order is reversed | `$params`, `$order`, `$items`, `$creditmemo` | | `taxcloud_returned_after` | Emitted after the `Returned` call when a credit memo is created or when a canceled unpaid order is reversed | `$result`, `$order`, `$items`, `$creditmemo` | diff --git a/etc/events.xml b/etc/events.xml index 978e28a..6c16bcc 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -18,7 +18,7 @@ --> - + From b39379b3482c6c9a8741c9769686e40d51058446 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 26 Feb 2026 07:16:56 -0800 Subject: [PATCH 2/4] DEV-6955 Added configurable when to send order to TaxCloud (order creation / payment / shipment) --- Model/Config/Source/CaptureTrigger.php | 39 +++++++++++++++ Observer/Sales/Complete.php | 69 ++++++++++++++++++++++++-- README.md | 3 +- etc/adminhtml/system.xml | 10 ++++ etc/config.xml | 1 + etc/events.xml | 8 ++- 6 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 Model/Config/Source/CaptureTrigger.php diff --git a/Model/Config/Source/CaptureTrigger.php b/Model/Config/Source/CaptureTrigger.php new file mode 100644 index 0000000..ce02eff --- /dev/null +++ b/Model/Config/Source/CaptureTrigger.php @@ -0,0 +1,39 @@ + + * @copyright 2021 The Federal Tax Authority, LLC d/b/a TaxCloud + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +namespace Taxcloud\Magento2\Model\Config\Source; + +use \Magento\Framework\Data\OptionSourceInterface; + +class CaptureTrigger implements OptionSourceInterface +{ + const ORDER_CREATION = 'order_creation'; + const PAYMENT = 'payment'; + const SHIPMENT = 'shipment'; + + /** + * @return array + */ + public function toOptionArray() + { + return [ + ['value' => self::ORDER_CREATION, 'label' => __('On order creation')], + ['value' => self::PAYMENT, 'label' => __('On payment')], + ['value' => self::SHIPMENT, 'label' => __('On shipment')], + ]; + } +} diff --git a/Observer/Sales/Complete.php b/Observer/Sales/Complete.php index e0fd52d..a646b11 100644 --- a/Observer/Sales/Complete.php +++ b/Observer/Sales/Complete.php @@ -19,6 +19,7 @@ use \Magento\Framework\Event\ObserverInterface; use \Magento\Framework\Event\Observer; +use Taxcloud\Magento2\Model\Config\Source\CaptureTrigger; class Complete implements ObserverInterface { @@ -69,6 +70,17 @@ public function info() } /** + * Event names that correspond to each capture trigger option. + */ + private static $triggerToEvent = [ + CaptureTrigger::ORDER_CREATION => 'sales_order_place_after', + CaptureTrigger::PAYMENT => 'sales_order_invoice_pay', + CaptureTrigger::SHIPMENT => 'sales_order_shipment_save_after', + ]; + + /** + * Run only when the current event matches the configured "When to send order to TaxCloud" setting. + * * @param Observer $observer */ public function execute( @@ -81,15 +93,64 @@ public function execute( return; } - $this->tclogger->info('Running Observer sales_order_invoice_pay (capture on payment)'); + $eventName = $observer->getEvent()->getName(); + $configuredTrigger = $this->scopeConfig->getValue( + 'tax/taxcloud_settings/capture_trigger', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($configuredTrigger === null || $configuredTrigger === '') { + $configuredTrigger = CaptureTrigger::ORDER_CREATION; + } + + $expectedEvent = isset(self::$triggerToEvent[$configuredTrigger]) + ? self::$triggerToEvent[$configuredTrigger] + : self::$triggerToEvent[CaptureTrigger::ORDER_CREATION]; + + if ($eventName !== $expectedEvent) { + return; + } - $order = $observer->getEvent()->getInvoice()->getOrder(); + $this->tclogger->info('Running Observer ' . $eventName . ' (capture trigger: ' . $configuredTrigger . ')'); - // Only send to TaxCloud on first invoice pay to avoid duplicate API calls for partial invoices - if ($order->getInvoiceCollection()->getSize() > 1) { + $order = $this->getOrderFromObserver($observer, $eventName); + if (!$order) { return; } $this->tcapi->authorizeCapture($order); } + + /** + * Get order from observer based on event. Returns null if we should skip (e.g. not first invoice/shipment). + * + * @param Observer $observer + * @param string $eventName + * @return \Magento\Sales\Model\Order|null + */ + private function getOrderFromObserver(Observer $observer, $eventName) + { + $event = $observer->getEvent(); + + if ($eventName === 'sales_order_place_after') { + return $event->getOrder(); + } + + if ($eventName === 'sales_order_invoice_pay') { + $order = $event->getInvoice()->getOrder(); + if ($order->getInvoiceCollection()->getSize() > 1) { + return null; + } + return $order; + } + + if ($eventName === 'sales_order_shipment_save_after') { + $order = $event->getShipment()->getOrder(); + if ($order->getShipmentCollection()->getSize() > 1) { + return null; + } + return $order; + } + + return null; + } } diff --git a/README.md b/README.md index a3ba6a7..267eccb 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Navigate to *Stores → Configuration* and then *Sales → Tax*. * **Default TIC** - Enter the Taxability Information Code you would like to use for products where an explicit TIC has not been specified. * **Shipping TIC** - Enter the Taxability Information Code you would like to use for shipping costs. Use `11010` if you charge only postage, and `11000` for shipping & handling. * **Cache Lifetime** - Enter the amount of time in seconds you would like to cache the sales tax lookup and verify address API calls. The default value is `86400` (24 hours), or enter `0` to disable caching for development purposes. +* **When to send order to TaxCloud** - Choose when the order is sent to TaxCloud: *On order creation* (at checkout; default), *On payment* (when an invoice is paid; recommended to avoid canceled orders reaching TaxCloud), or *On shipment* (when a shipment is created). For online payment methods, "on creation" and "on payment" often fire together; the choice matters for offline payment or when you only want to report tax on fulfilled orders. #### Product Settings @@ -237,8 +238,6 @@ In certain cases, a store owner may need to extend this module. Specific use cas Each of these situations can be accomplished using an event observer. For every API call to TaxCloud, this module emits a before and after event. The before events can be used to modify the parameters sent to TaxCloud's API, and the after events can be used to modify the response. -**Capture on payment:** Orders are sent to TaxCloud when the order is **captured** (first invoice paid), not when the order is placed. Canceled orders never reach TaxCloud, avoiding the need for void/refund workarounds (TaxCloud has no native "Canceled" state). This aligns with a capture-on-payment/fulfillment flow used on other platforms. - | Event Name | Description | Data Objects | | ----- | ---- | --- | | `taxcloud_lookup_before` | Emitted before the `Lookup` call to get tax rates | `$params`, `$customer`, `$address`, `$quote`, `$itemsByType`, `$shippingAssignment` | diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index c6c1710..51acf70 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -139,6 +139,16 @@ + + + Taxcloud\Magento2\Model\Config\Source\CaptureTrigger + tax/taxcloud_settings/capture_trigger + On order creation: at checkout (default; canceled orders may reach TaxCloud). On payment: when invoice is paid (recommended to avoid canceled orders). On shipment: when shipment is created. + + 1 + + + diff --git a/etc/config.xml b/etc/config.xml index 7c05a46..73e36ed 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -28,6 +28,7 @@ 11010 86400 0 + order_creation diff --git a/etc/events.xml b/etc/events.xml index 6c16bcc..5509dc2 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -18,9 +18,15 @@ --> - + + + + + + + From ca008b035b699b04bc574d61378834e45a2d96f2 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 26 Feb 2026 11:30:09 -0800 Subject: [PATCH 3/4] DEV-6955 Changed the field label to Capture in TaxCloud --- Observer/Sales/Complete.php | 2 +- README.md | 2 +- etc/adminhtml/system.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Observer/Sales/Complete.php b/Observer/Sales/Complete.php index a646b11..661757d 100644 --- a/Observer/Sales/Complete.php +++ b/Observer/Sales/Complete.php @@ -79,7 +79,7 @@ public function info() ]; /** - * Run only when the current event matches the configured "When to send order to TaxCloud" setting. + * Run only when the current event matches the configured "Capture in TaxCloud" setting. * * @param Observer $observer */ diff --git a/README.md b/README.md index 267eccb..4608400 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Navigate to *Stores → Configuration* and then *Sales → Tax*. * **Default TIC** - Enter the Taxability Information Code you would like to use for products where an explicit TIC has not been specified. * **Shipping TIC** - Enter the Taxability Information Code you would like to use for shipping costs. Use `11010` if you charge only postage, and `11000` for shipping & handling. * **Cache Lifetime** - Enter the amount of time in seconds you would like to cache the sales tax lookup and verify address API calls. The default value is `86400` (24 hours), or enter `0` to disable caching for development purposes. -* **When to send order to TaxCloud** - Choose when the order is sent to TaxCloud: *On order creation* (at checkout; default), *On payment* (when an invoice is paid; recommended to avoid canceled orders reaching TaxCloud), or *On shipment* (when a shipment is created). For online payment methods, "on creation" and "on payment" often fire together; the choice matters for offline payment or when you only want to report tax on fulfilled orders. +* **Capture in TaxCloud** - Choose when the order is sent to TaxCloud: *On order creation* (at checkout; default), *On payment* (when an invoice is paid; recommended to avoid canceled orders reaching TaxCloud), or *On shipment* (when a shipment is created). For online payment methods, "on creation" and "on payment" often fire together; the choice matters for offline payment or when you only want to report tax on fulfilled orders. #### Product Settings diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 51acf70..bc935e4 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -140,7 +140,7 @@ - + Taxcloud\Magento2\Model\Config\Source\CaptureTrigger tax/taxcloud_settings/capture_trigger On order creation: at checkout (default; canceled orders may reach TaxCloud). On payment: when invoice is paid (recommended to avoid canceled orders). On shipment: when shipment is created. From 47f644258c01462831c9d575fb04076fbe98c330 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 6 Mar 2026 11:43:57 -0800 Subject: [PATCH 4/4] DEV-6955 Added constants for event names --- Observer/Sales/Complete.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Observer/Sales/Complete.php b/Observer/Sales/Complete.php index 661757d..2f0aafd 100644 --- a/Observer/Sales/Complete.php +++ b/Observer/Sales/Complete.php @@ -23,6 +23,14 @@ class Complete implements ObserverInterface { + /** @var string Magento event: order placed */ + const EVENT_ORDER_PLACE_AFTER = 'sales_order_place_after'; + + /** @var string Magento event: invoice paid */ + const EVENT_INVOICE_PAY = 'sales_order_invoice_pay'; + + /** @var string Magento event: shipment saved */ + const EVENT_SHIPMENT_SAVE_AFTER = 'sales_order_shipment_save_after'; /** * Core store config @@ -73,9 +81,9 @@ public function info() * Event names that correspond to each capture trigger option. */ private static $triggerToEvent = [ - CaptureTrigger::ORDER_CREATION => 'sales_order_place_after', - CaptureTrigger::PAYMENT => 'sales_order_invoice_pay', - CaptureTrigger::SHIPMENT => 'sales_order_shipment_save_after', + CaptureTrigger::ORDER_CREATION => self::EVENT_ORDER_PLACE_AFTER, + CaptureTrigger::PAYMENT => self::EVENT_INVOICE_PAY, + CaptureTrigger::SHIPMENT => self::EVENT_SHIPMENT_SAVE_AFTER, ]; /** @@ -131,11 +139,11 @@ private function getOrderFromObserver(Observer $observer, $eventName) { $event = $observer->getEvent(); - if ($eventName === 'sales_order_place_after') { + if ($eventName === self::EVENT_ORDER_PLACE_AFTER) { return $event->getOrder(); } - if ($eventName === 'sales_order_invoice_pay') { + if ($eventName === self::EVENT_INVOICE_PAY) { $order = $event->getInvoice()->getOrder(); if ($order->getInvoiceCollection()->getSize() > 1) { return null; @@ -143,7 +151,7 @@ private function getOrderFromObserver(Observer $observer, $eventName) return $order; } - if ($eventName === 'sales_order_shipment_save_after') { + if ($eventName === self::EVENT_SHIPMENT_SAVE_AFTER) { $order = $event->getShipment()->getOrder(); if ($order->getShipmentCollection()->getSize() > 1) { return null;