Feature/order reissue (#910)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Add change to changelog

* Remove console.log

* Update tests

* Extract messages

* Add utils functions for selecting only ulfulfilled order lines

* Add optional value selection for line item

* Update
tests

* Add optional rendering of unfulfilled items card and refactor a bit

* Update displaying of items card title when refunded card

* UUpdate utils, form data etc. not to include refunded items when calculating replaced items amount

* Uppdate return items card not to display replace buttons for refunded items

* Refactor and small fixes after review

* Update extracted messages

* Fix card title when no fullfilemtn id

* wip

* Initially stitch returns page. Update types, add mutation

* remove unnecessary component display names

* Add loading status from form submission & refactor

* Add errors from response

* Add errors from response and refactor

* Remove comments

* Add optional error adding when no data from return create request

* Update messages

* wip

* Update snapshots

* Remove unnecessary console.log

* Add better typing for getParsedLineData function

* Update & refactor card title to match cards both in return and order details

* Add handling of new statuses to order details cards. Also refactor, and devide order fulfillment card into couple of smaller components

* Update messages

* Update schema to match api

* Update types

* Update status label component to match colors with new designs and order details cards

* RUpdate and refactor order fulfillment card components to be reusable. Also add replaced status handling

* Updayte card title component to handle all cases and statuses

* Update oorder unfulfilled items card and order details page, reduce some of the boilerplate

* Fix card title types and adjust returns card to match

* Update messages

* Update snapshots

* RUpdate order fulfillment card with subtitles and buttons for returned status

* Add onRefund to order fulfillment card

* Fix typo and wrong message in card title

* Add missing condition in return form submission utils to decice if to refund products

* Update fulfillment subtitles row and tests

* Update messages

* Change naming and locations of OrderFulfillment and items card components

* Update messages

* U[pdate names of components again to even better ones

* Update messages

* changelog

* Update schema and types so that order history event also includes user first and last name

* Add extended timeline event and event header components. Move some of the logic to utils and add way to display links in the event header.

* FFix types

* Update messages

* Change naming of isOfType -> isTimelineEventOfType and refactor extended timeline event messages selection to be less complicated

* Add ids and update messages

* Add ids and update messages some more

* Update storybook decorator to work with react router context in components and tests

* Refactor after review

* Update messages

* Add rredirecting to draft order

* Add handling draft creation from replacement

* Add related order to order event fragment and update lots and lots of types

* Update extended timeline event to match related order type on order history event

* Update fixtures

* Refactor ExtendedTimelineEvent

Co-authored-by: Jakub Majorek <majorek.jakub@gmail.com>

* Fix typing

* Update messages

* Fix missing history event for replacement draft created for replaced products

* Update messages

* Handle new statuses for returned and partially returned orders

* Update messages

* update snapshots

* BBump empty line to rebuild ci

* Change status to proper color

* Change replaceable items in return for replace to be auto off instead of on

* Add utils functions and make order details menu not show option to return items when there are returnable items in the order

* Fix replace checkbox showing when previously hidden and clicked set maximal quantities

* Fix return form invalid money values

* Add default values to avoid returning of NaN in utils for return amount and refactor

* Add ggeneral error alerts

* Add eproduct error box component and style. style a lot.

* Fixes

* Fix lint

* Add cannot refund error title + description

* Extract messages

* Refactor after review

* Add better, nicer and fancier imports to product error cell

* Use error color from palette in product error cell

* Fix max refund when 0 for return

* Add ddisable ability to refund products button so it's disabled when 0 products selected

* Add class for order return form data parsing and add condition to not do refund when total captured on order is 0

* Update snapshots

* Add condition for order lines quantity in order products table row

* Fix return amount submit button

* Add change to changelog

Co-authored-by: Jakub Majorek <majorek.jakub@gmail.com>
This commit is contained in:
mmarkusik 2021-01-20 17:16:43 +01:00 committed by GitHub
parent b07bb08ade
commit f0f9fe9b85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 6819 additions and 4215 deletions

View file

@ -9,6 +9,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add shipping methods to translation section - #864 by @marekchoinski
- New Miscellaneous and Product refunds - #870 by @orzechdev
- Add zip code exclusion - #877 by @dominik-zeglen
- Add order reissue
- Update quantity column in Inventory part of Product Variant view - #904 by @dominik-zeglen
- Add file attributes - #884 by @orzechdev
- Add shipping delivery days - #914 by @orzechdev

View file

@ -3,6 +3,14 @@
"context": "dialog header",
"string": "Cancel Order"
},
"amount title": {
"context": "amount title",
"string": "Refunded amount"
},
"by preposition": {
"context": "by preposition",
"string": "by"
},
"configurationMenuAttributes": {
"string": "Determine attributes used to create product types"
},
@ -42,6 +50,38 @@
"configurationPluginsPages": {
"string": "View and update your plugins and their settings."
},
"event products list title refunded": {
"context": "refunded products list title",
"string": "Products refunded"
},
"event products list title replaced": {
"context": "replaced products list title",
"string": "Products replaced"
},
"event products list title returned": {
"context": "returned products list title",
"string": "Products returned"
},
"event products title draft reissued": {
"context": "draft created from replace products list title",
"string": "Products replaced"
},
"event title draft reissued": {
"context": "draft created from replace event title",
"string": "Draft was reissued from order "
},
"event title refunded": {
"context": "refunded event title",
"string": "Products were refunded by "
},
"event title replaced": {
"context": "replaced event title",
"string": "Products were replaced by "
},
"event title returned": {
"context": "returned event title",
"string": "Products were returned by"
},
"homeActivityCardHeader": {
"context": "header",
"string": "Activity"
@ -336,6 +376,10 @@
"context": "unassign product from sale, button",
"string": "Unassign"
},
"shipment refund title": {
"context": "shipment refund title",
"string": "Shipment was refunded"
},
"shippingZoneDetailsDialogsDeleteShippingMethod": {
"context": "delete shipping method",
"string": "Are you sure you want to delete {name}?"
@ -3164,13 +3208,17 @@
"src_dot_orders_dot_components_dot_OrderCustomer_dot_4282475982": {
"string": "Billing Address"
},
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_1854613983": {
"context": "button",
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_cancelOrder": {
"context": "cancel button",
"string": "Cancel order"
},
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_3086420445": {
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_confirmOrder": {
"context": "save button",
"string": "confirm order"
"string": "Confirm order"
},
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_returnOrder": {
"context": "return button",
"string": "Return / Replace order"
},
"src_dot_orders_dot_components_dot_OrderDraftCancelDialog_dot_1961675716": {
"context": "dialog header",
@ -3306,6 +3354,37 @@
"context": "product's sku",
"string": "SKU"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_1119771899": {
"context": "add tracking button",
"string": "Add tracking"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_2211099657": {
"context": "edit tracking button",
"string": "Edit tracking"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_2845258362": {
"context": "refund button",
"string": "Refund"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_3254150098": {
"string": "Tracking Number: {trackingNumber}"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_732594284": {
"context": "button",
"string": "Cancel Fulfillment"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_fulfilled": {
"context": "fulfillment group",
"string": "Fulfilled from: "
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_restocked": {
"context": "restocked group",
"string": "Restocked from: "
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_tracking": {
"context": "tracking number",
"string": "Tracking Number: {trackingNumber}"
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_1097287358": {
"string": "Are you sure you want to cancel fulfillment? Canceling a fulfillment will restock products at a selected warehouse."
},
@ -3343,71 +3422,10 @@
"context": "dialog header",
"string": "Add Tracking Code"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_1119771899": {
"context": "fulfillment group tracking number",
"string": "Add tracking"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_1134347598": {
"context": "product price",
"string": "Price"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_1895667608": {
"context": "product name",
"string": "Product"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_2211099657": {
"context": "fulfillment group tracking number",
"string": "Edit tracking"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_2567258278": {
"context": "refunded fulfillment, section header",
"string": "Refunded ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_2796503714": {
"context": "ordered product quantity",
"string": "Quantity"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_3254150098": {
"string": "Tracking Number: {trackingNumber}"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_3494686506": {
"context": "section header",
"string": "Fulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_4039425374": {
"context": "cancelled fulfillment, section header",
"string": "Cancelled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_693960049": {
"context": "ordered product sku",
"string": "SKU"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_732594284": {
"context": "button",
"string": "Cancel Fulfillment"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_77179533": {
"context": "fulfillment group",
"string": "Fulfilled from: {warehouseName}"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_878013594": {
"context": "order line total price",
"string": "Total"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_1154330234": {
"context": "transaction reference",
"string": "Transaction Reference {transactionReference}"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_1230178536": {
"context": "order history message",
"string": "Order address was updated"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_123236698": {
"string": "Shipment was refunded"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_1322321687": {
"string": "Refunded amount"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_1463685940": {
"context": "order history message",
"string": "Order was marked as paid"
@ -3523,9 +3541,6 @@
"context": "order history message",
"string": "Payment failed"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_492197448": {
"string": "Products refunded"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_493321552": {
"context": "order history message",
"string": "Order cancel information was sent to customer"
@ -3546,6 +3561,14 @@
"context": "order history message",
"string": "Order was cancelled"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_description": {
"context": "replacement created order history message description",
"string": "was created for replaced products"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_draftNumber": {
"context": "replacement created order history message draft number",
"string": "Draft #{orderNumber} "
},
"src_dot_orders_dot_components_dot_OrderInvoiceEmailSendDialog_dot_1821123638": {
"string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?"
},
@ -3729,71 +3752,25 @@
"src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": {
"string": "No products matching given query"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1580639738": {
"context": "order refund amount",
"string": "Proposed refund amount"
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1134347598": {
"context": "product price",
"string": "Price"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1705174606": {
"context": "order refund amount",
"string": "Max Refund"
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1895667608": {
"context": "product name",
"string": "Product"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1734445951": {
"context": "order refund amount",
"string": "Refund total amount"
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_2796503714": {
"context": "ordered product quantity",
"string": "Quantity"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2045860028": {
"context": "order refund amount",
"string": "Authorized Amount"
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_693960049": {
"context": "ordered product sku",
"string": "SKU"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2854815744": {
"context": "order refund amount",
"string": "Previously refunded"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2907874606": {
"context": "order refund amount",
"string": "Selected products value"
},
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_79173946": {
"context": "order refund amount",
"string": "Shipment cost"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_120912052": {
"context": "order refund amount",
"string": "Refunded items cant be fulfilled"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_159210811": {
"string": "Amount must be bigger than 0"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_2256869831": {
"context": "section header",
"string": "Refunded Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_2845258362": {
"context": "order refund amount, input button",
"string": "Refund"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_4033685232": {
"string": "Amount cannot be bigger than max refund"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_40513382": {
"context": "order refund amount, input button",
"string": "Refund {currency} {amount}"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_4224226791": {
"context": "label",
"string": "Automatic Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_508357513": {
"context": "label",
"string": "Manual Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_553737700": {
"context": "checkbox",
"string": "Refund shipment costs"
},
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_75546233": {
"context": "order refund amount, input label",
"string": "Amount"
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_878013594": {
"context": "order line total price",
"string": "Total"
},
"src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1134347598": {
"context": "tabel column header",
@ -3838,6 +3815,86 @@
"context": "page header with order number",
"string": "Order #{orderNumber}"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_2256869831": {
"context": "section header",
"string": "Refunded Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_40513382": {
"context": "order refund amount, input button",
"string": "Refund {currency} {amount}"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_4224226791": {
"context": "label",
"string": "Automatic Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_508357513": {
"context": "label",
"string": "Manual Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_553737700": {
"context": "checkbox",
"string": "Refund shipment costs"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_amountTooBig": {
"context": "Amount error message",
"string": "Amount cannot be bigger than max refund"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_amountTooSmall": {
"context": "Amount error message",
"string": "Amount must be bigger than 0"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_authorizedAmount": {
"context": "order refund amount",
"string": "Authorized Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_label": {
"context": "order refund amount, input label",
"string": "Amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_maxRefund": {
"context": "order refund amount",
"string": "Max Refund"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_previouslyRefunded": {
"context": "order refund amount",
"string": "Previously refunded"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_proposedRefundAmount": {
"context": "order refund amount",
"string": "Proposed refund amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundButton": {
"context": "order refund amount button",
"string": "Refund"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundCannotBeFulfilled": {
"context": "order refund subtitle",
"string": "Refunded items can't be fulfilled"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundTotalAmount": {
"context": "order refund amount",
"string": "Refund total amount"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_replacedProductsValue": {
"context": "order refund amount",
"string": "Replaced Products Value"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_returnButton": {
"context": "order return amount button",
"string": "Return & Replace products"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_returnCannotBeFulfilled": {
"context": "order return subtitle",
"string": "Returned items can't be fulfilled"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_selectedProductsValue": {
"context": "order refund amount",
"string": "Selected Products Value"
},
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_shipmentCost": {
"context": "order refund amount",
"string": "Shipment Cost"
},
"src_dot_orders_dot_components_dot_OrderRefundUnfulfilledProducts_dot_1134347598": {
"context": "tabel column header",
"string": "Price"
@ -3885,6 +3942,82 @@
"context": "refund type",
"string": "Refund Products"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1134347598": {
"context": "table column header",
"string": "Price"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1784788864": {
"context": "table column header",
"string": "Return"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1895667608": {
"context": "table column header",
"string": "Product"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_2049070632": {
"context": "table column header",
"string": "Replace"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_3988345170": {
"context": "button",
"string": "Set maximal quantities"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_cancelled": {
"context": "cancelled fulfillment, section header",
"string": "Cancelled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_description": {
"context": "product no longer exists error description",
"string": "This product is no longer in database so it cant be replaced, nor returned"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_fulfilled": {
"context": "section header",
"string": "Fulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_improperValue": {
"context": "error message",
"string": "Improper value"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refunded": {
"context": "refunded fulfillment, section header",
"string": "Refunded ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refundedAndReturned": {
"context": "cancelled fulfillment, section header",
"string": "Refunded and Returned ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_replaced": {
"context": "refunded fulfillment, section header",
"string": "Replaced ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_returned": {
"context": "refunded fulfillment, section header",
"string": "Returned ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_title": {
"context": "product no longer exists error title",
"string": "Product no longer exists"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_titleFulfilled": {
"context": "section header",
"string": "Fulfillment - #{fulfilmentId}"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_titleUnfulfilled": {
"context": "section header",
"string": "Unfulfilled Items"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_unfulfilled": {
"context": "section header",
"string": "Unfulfilled"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_appTitle": {
"context": "page header with order number",
"string": "Order #{orderNumber}"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_pageTitle": {
"context": "page header",
"string": "Order no. {orderNumber} - Replace/Return"
},
"src_dot_orders_dot_components_dot_OrderSettingsPage_dot_1149215359": {
"context": "header",
"string": "Order settings"
@ -3908,34 +4041,10 @@
"context": "dialog header",
"string": "Edit Shipping Method"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1134347598": {
"context": "product unit price",
"string": "Price"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1895667608": {
"context": "product name",
"string": "Product"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2095687440": {
"src_dot_orders_dot_components_dot_OrderUnfulfilledProductsCard_dot_2095687440": {
"context": "button",
"string": "Fulfill"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2796503714": {
"context": "ordered products",
"string": "Quantity"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2886647373": {
"context": "section header",
"string": "Unfulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_693960049": {
"context": "ordered product sku",
"string": "SKU"
},
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_878013594": {
"context": "order line total price",
"string": "Total"
},
"src_dot_orders_dot_views_dot_OrderDetails_dot_1039259580": {
"string": "Were generating the invoice you requested. Please wait a couple of moments"
},
@ -4025,6 +4134,18 @@
"context": "order refunded success message",
"string": "Refunded Items"
},
"src_dot_orders_dot_views_dot_OrderReturn_dot_cannotRefundDescription": {
"context": "order return error description when cannot refund",
"string": "Weve encountered a problem while refunding the products. Products were not refunded. Please try again."
},
"src_dot_orders_dot_views_dot_OrderReturn_dot_cannotRefundTitle": {
"context": "order return error title when cannot refund",
"string": "Couldn't refund products"
},
"src_dot_orders_dot_views_dot_OrderReturn_dot_successAlert": {
"context": "order returned success message",
"string": "Successfully returned products!"
},
"src_dot_pageTypes": {
"context": "page types section name",
"string": "Page Types"
@ -4271,6 +4392,10 @@
"context": "payment status",
"string": "Partially refunded"
},
"src_dot_partiallyReturned": {
"context": "order status",
"string": "Partially returned"
},
"src_dot_permissionGroups": {
"context": "permission groups section name",
"string": "Permission Groups"
@ -5306,6 +5431,10 @@
"src_dot_requiredField": {
"string": "This field is required"
},
"src_dot_returned": {
"context": "order status",
"string": "Returned"
},
"src_dot_sales": {
"context": "sales section name",
"string": "Sales"

View file

@ -2006,9 +2006,21 @@ type FulfillmentRefundProducts {
orderErrors: [OrderError!]!
}
type FulfillmentReturnProducts {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
returnFulfillment: Fulfillment
replaceFulfillment: Fulfillment
order: Order
replaceOrder: Order
orderErrors: [OrderError!]!
}
enum FulfillmentStatus {
FULFILLED
REFUNDED
RETURNED
REPLACED
REFUNDED_AND_RETURNED
CANCELED
}
@ -2662,6 +2674,7 @@ type Mutation {
orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel
orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking
orderFulfillmentRefundProducts(input: OrderRefundProductsInput!, order: ID!): FulfillmentRefundProducts
orderFulfillmentReturnProducts(input: OrderReturnProductsInput!, order: ID!): FulfillmentReturnProducts
orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid
orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund
orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate
@ -2959,8 +2972,7 @@ enum OrderErrorCode {
UNIQUE
VOID_INACTIVE_PAYMENT
ZERO_QUANTITY
INVALID_REFUND_QUANTITY
CANNOT_REFUND_FULFILLMENT_LINE
INVALID_QUANTITY
INSUFFICIENT_STOCK
DUPLICATED_INPUT_ITEM
NOT_AVAILABLE_IN_CHANNEL
@ -2988,6 +3000,7 @@ type OrderEvent implements Node {
warehouse: Warehouse
transactionReference: String
shippingCostsIncluded: Boolean
relatedOrder: Order
}
type OrderEventCountableConnection {
@ -3021,6 +3034,7 @@ enum OrderEventsEmailsEnum {
enum OrderEventsEnum {
DRAFT_CREATED
DRAFT_CREATED_FROM_REPLACE
DRAFT_ADDED_PRODUCTS
DRAFT_REMOVED_PRODUCTS
PLACED
@ -3029,6 +3043,7 @@ enum OrderEventsEnum {
CANCELED
ORDER_MARKED_AS_PAID
ORDER_FULLY_PAID
ORDER_REPLACEMENT_CREATED
UPDATED_ADDRESS
EMAIL_SENT
CONFIRMED
@ -3046,6 +3061,8 @@ enum OrderEventsEnum {
FULFILLMENT_RESTOCKED_ITEMS
FULFILLMENT_FULFILLED_ITEMS
FULFILLMENT_REFUNDED
FULFILLMENT_RETURNED
FULFILLMENT_REPLACED
TRACKING_UPDATED
NOTE_ADDED
OTHER
@ -3139,6 +3156,26 @@ input OrderRefundProductsInput {
includeShippingCosts: Boolean = false
}
input OrderReturnFulfillmentLineInput {
fulfillmentLineId: ID!
quantity: Int!
replace: Boolean = false
}
input OrderReturnLineInput {
orderLineId: ID!
quantity: Int!
replace: Boolean = false
}
input OrderReturnProductsInput {
orderLines: [OrderReturnLineInput!]
fulfillmentLines: [OrderReturnFulfillmentLineInput!]
amountToRefund: PositiveDecimal
includeShippingCosts: Boolean = false
refund: Boolean = false
}
type OrderSettings {
automaticallyConfirmAllNewOrders: Boolean!
}
@ -3181,6 +3218,8 @@ enum OrderStatus {
UNCONFIRMED
UNFULFILLED
PARTIALLY_FULFILLED
PARTIALLY_RETURNED
RETURNED
FULFILLED
CANCELED
}

View file

@ -19,11 +19,14 @@ const useStyles = makeStyles(
};
return {
alertDot: {
"&:before": { backgroundColor: yellow[500], ...dot }
},
errorDot: {
"&:before": { backgroundColor: theme.palette.error.main, ...dot }
},
neutralDot: {
"&:before": { backgroundColor: yellow[500], ...dot }
"&:before": { backgroundColor: grey[300], ...dot }
},
root: {
display: "inline-block",
@ -35,9 +38,6 @@ const useStyles = makeStyles(
},
successDot: {
"&:before": { backgroundColor: theme.palette.primary.main, ...dot }
},
unspecifiedDot: {
"&:before": { backgroundColor: grey[500], ...dot }
}
};
},
@ -47,7 +47,7 @@ const useStyles = makeStyles(
interface StatusLabelProps {
className?: string;
label: string | React.ReactNode;
status: "success" | "neutral" | "unspecified" | "error" | string;
status: "success" | "alert" | "neutral" | "error" | string;
typographyProps?: TypographyProps;
}
@ -62,8 +62,8 @@ const StatusLabel: React.FC<StatusLabelProps> = props => {
[classes.root]: true,
[className]: true,
[classes.successDot]: status === "success",
[classes.alertDot]: status === "alert",
[classes.neutralDot]: status === "neutral",
[classes.unspecifiedDot]: status === "unspecified",
[classes.errorDot]: status === "error"
})}
>

View file

@ -77,7 +77,7 @@ export const light: IThemeColors = {
default: "#616161"
},
divider: "#EAEAEA",
error: "#C22D74",
error: "#FE6D76",
font: {
button: "#FFFFFF",
default: "#3D3D3D",

View file

@ -4,30 +4,12 @@ import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import classNames from "classnames";
import React from "react";
import { DateTime } from "../Date";
import TimelineEventHeader, { TitleElement } from "./TimelineEventHeader";
const useStyles = makeStyles(
theme => ({
container: {
alignItems: "center",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
marginBottom: theme.spacing(),
marginLeft: theme.spacing(3),
width: "100%"
},
date: {
color: theme.typography.caption.color
},
dateExpander: {
color: theme.typography.caption.color,
position: "absolute",
right: 0
},
dot: {
backgroundColor: theme.palette.primary.main,
borderRadius: "100%",
@ -37,27 +19,27 @@ const useStyles = makeStyles(
top: 6,
width: 8
},
expanded: {},
noExpander: {
alignItems: "center",
display: "flex",
justifyContent: "space-between",
width: "100%"
},
panel: {
"&$expanded": {
margin: 0
"& .MuiExpansionPanelDetails-root": {
padding: 0,
paddingTop: theme.spacing(2)
},
"&.Mui-expanded": {
borderColor: "red",
margin: 0,
minHeight: 0
},
"&:before": {
display: "none"
},
background: "none",
display: "",
margin: 0,
minHeight: 0,
width: "100%"
},
panelExpander: {
"&$expanded": {
margin: 0,
"&.MuiExpansionPanelSummary-root.Mui-expanded": {
minHeight: 0
},
"&> .MuiExpansionPanelSummary-content": {
@ -65,9 +47,12 @@ const useStyles = makeStyles(
},
"&> .MuiExpansionPanelSummary-expandIcon": {
padding: 0,
position: "absolute",
right: theme.spacing(18)
},
margin: 0
margin: 0,
minHeight: 0,
padding: 0
},
root: {
"&:last-child:after": {
@ -82,27 +67,24 @@ const useStyles = makeStyles(
alignItems: "center",
display: "flex",
marginBottom: theme.spacing(3),
marginTop: 0,
position: "relative",
width: "100%"
},
secondaryTitle: {
color: "#9e9e9e",
fontSize: 14,
marginTop: theme.spacing(2)
}
}),
{ name: "TimelineEvent" }
);
interface TimelineEventProps {
export interface TimelineEventProps {
children?: React.ReactNode;
date: string;
secondaryTitle?: string;
title: string;
title?: string;
titleElements?: TitleElement[];
}
export const TimelineEvent: React.FC<TimelineEventProps> = props => {
const { children, date, secondaryTitle, title } = props;
const { children, date, secondaryTitle, title, titleElements } = props;
const classes = useStyles(props);
@ -110,39 +92,28 @@ export const TimelineEvent: React.FC<TimelineEventProps> = props => {
<div className={classes.root}>
<span className={classes.dot} />
{children ? (
<ExpansionPanel
className={classNames(classes.panel, classes.expanded)}
elevation={0}
>
<ExpansionPanel className={classes.panel} elevation={0}>
<ExpansionPanelSummary
className={classNames(classes.panelExpander, classes.expanded)}
className={classes.panelExpander}
expandIcon={<ExpandMoreIcon />}
>
<Typography>{title}</Typography>
<Typography className={classes.dateExpander}>
<DateTime date={date} />
</Typography>
<TimelineEventHeader
title={title}
date={date}
titleElements={titleElements}
/>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>{children}</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
) : (
<div className={classes.container}>
<div className={classes.noExpander}>
<Typography>{title}</Typography>
<Typography className={classes.date}>
<DateTime date={date} />
</Typography>
</div>
{secondaryTitle && (
<div className={classes.noExpander}>
<Typography className={classes.secondaryTitle}>
{secondaryTitle}
</Typography>
</div>
)}
</div>
<TimelineEventHeader
title={title}
titleElements={titleElements}
secondaryTitle={secondaryTitle}
date={date}
/>
)}
</div>
);

View file

@ -0,0 +1,93 @@
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import useNavigator from "@saleor/hooks/useNavigator";
import React from "react";
import { DateTime } from "../Date";
import Link from "../Link";
const useStyles = makeStyles(
theme => ({
container: {
alignItems: "center",
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
width: "100%"
},
date: {
color: theme.typography.caption.color,
paddingLeft: 24
},
elementsContainer: {
alignItems: "center",
display: "flex",
flexDirection: "row",
flexWrap: "wrap"
},
secondaryTitle: {
color: "#9e9e9e",
fontSize: 14,
marginTop: theme.spacing(2)
},
titleElement: {
marginRight: "0.5rem"
}
}),
{ name: "TimelineEventHeader" }
);
export interface TitleElement {
text: string;
link?: string;
}
export interface TimelineEventHeaderProps {
title?: string;
date: string;
titleElements?: TitleElement[];
secondaryTitle?: string;
}
export const TimelineEventHeader: React.FC<TimelineEventHeaderProps> = props => {
const { title, date, titleElements, secondaryTitle } = props;
const navigate = useNavigator();
const classes = useStyles(props);
return (
<div className={classes.container}>
{title && <Typography>{title}</Typography>}
{titleElements && (
<div className={classes.elementsContainer}>
{titleElements.map(({ text, link }) => {
if (link) {
return (
<Link
className={classes.titleElement}
onClick={() => navigate(link)}
>
{text}
</Link>
);
}
return (
<Typography className={classes.titleElement}>{text}</Typography>
);
})}
</div>
)}
<Typography className={classes.date}>
<DateTime date={date} />
</Typography>
{secondaryTitle && (
<Typography className={classes.secondaryTitle}>
{secondaryTitle}
</Typography>
)}
</div>
);
};
export default TimelineEventHeader;

View file

@ -12,6 +12,10 @@ export const fragmentOrderEvent = gql`
email
emailType
invoiceNumber
relatedOrder {
id
number
}
message
quantity
transactionReference
@ -19,6 +23,8 @@ export const fragmentOrderEvent = gql`
user {
id
email
firstName
lastName
}
lines {
quantity

View file

@ -42,10 +42,18 @@ export interface OrderDetailsFragment_billingAddress {
streetAddress2: string;
}
export interface OrderDetailsFragment_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderDetailsFragment_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderDetailsFragment_events_lines_orderLine {
@ -70,6 +78,7 @@ export interface OrderDetailsFragment_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderDetailsFragment_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -8,10 +8,18 @@ import { OrderEventsEmailsEnum, OrderEventsEnum } from "./../../types/globalType
// GraphQL fragment: OrderEventFragment
// ====================================================
export interface OrderEventFragment_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderEventFragment_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderEventFragment_lines_orderLine {
@ -36,6 +44,7 @@ export interface OrderEventFragment {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderEventFragment_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -0,0 +1,25 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
const ErrorExclamationCircle = createSvgIcon(
<>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="12" fill="#FE6D76" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.75 6H11.25V15H12.75V6ZM12.75 16.5H11.25V18H12.75V16.5Z"
fill="white"
/>
</svg>
</>,
"ErrorExclamationCircle"
);
export default ErrorExclamationCircle;

View file

@ -136,6 +136,10 @@ export const orderStatusMessages = defineMessages({
defaultMessage: "Partially fulfilled",
description: "order status"
},
partiallyReturned: {
defaultMessage: "Partially returned",
description: "order status"
},
readyToCapture: {
defaultMessage: "Ready to capture",
description: "order status"
@ -144,6 +148,10 @@ export const orderStatusMessages = defineMessages({
defaultMessage: "Ready to fulfill",
description: "order status"
},
returned: {
defaultMessage: "Returned",
description: "order status"
},
unconfirmed: {
defaultMessage: "Unconfirmed",
description: "order status"
@ -189,6 +197,16 @@ export const transformOrderStatus = (
localized: intl.formatMessage(orderStatusMessages.unconfirmed),
status: StatusType.NEUTRAL
};
case OrderStatus.PARTIALLY_RETURNED:
return {
localized: intl.formatMessage(orderStatusMessages.partiallyReturned),
status: StatusType.NEUTRAL
};
case OrderStatus.RETURNED:
return {
localized: intl.formatMessage(orderStatusMessages.returned),
status: StatusType.NEUTRAL
};
}
return {
localized: status,

View file

@ -19,19 +19,20 @@ import { UserPermissionProps } from "@saleor/types";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react";
import { useIntl } from "react-intl";
import { defineMessages, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc";
import { maybe } from "../../../misc";
import { OrderStatus } from "../../../types/globalTypes";
import { OrderDetails_order } from "../../types/OrderDetails";
import OrderCustomer from "../OrderCustomer";
import OrderCustomerNote from "../OrderCustomerNote";
import OrderFulfillment from "../OrderFulfillment";
import OrderFulfilledProductsCard from "../OrderFulfilledProductsCard";
import OrderHistory, { FormData as HistoryFormData } from "../OrderHistory";
import OrderInvoiceList from "../OrderInvoiceList";
import OrderPayment from "../OrderPayment/OrderPayment";
import OrderUnfulfilledItems from "../OrderUnfulfilledItems/OrderUnfulfilledItems";
import OrderUnfulfilledProductsCard from "../OrderUnfulfilledProductsCard";
import Title from "./Title";
import { filteredConditionalItems, hasAnyItemsReplaceable } from "./utils";
const useStyles = makeStyles(
theme => ({
@ -75,12 +76,28 @@ export interface OrderDetailsPageProps extends UserPermissionProps {
onOrderCancel();
onNoteAdd(data: HistoryFormData);
onProfileView();
onOrderReturn();
onInvoiceClick(invoiceId: string);
onInvoiceGenerate();
onInvoiceSend(invoiceId: string);
onSubmit(data: MetadataFormData): SubmitPromise;
}
const messages = defineMessages({
cancelOrder: {
defaultMessage: "Cancel order",
description: "cancel button"
},
confirmOrder: {
defaultMessage: "Confirm order",
description: "save button"
},
returnOrder: {
defaultMessage: "Return / Replace order",
description: "return button"
}
});
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
const {
disabled,
@ -103,6 +120,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
onInvoiceClick,
onInvoiceGenerate,
onInvoiceSend,
onOrderReturn,
onSubmit
} = props;
const classes = useStyles(props);
@ -140,10 +158,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
const saveLabel =
order?.status === OrderStatus.UNCONFIRMED
? intl.formatMessage({
defaultMessage: "confirm order",
description: "save button"
})
? intl.formatMessage(messages.confirmOrder)
: undefined;
const allowSave = (hasChanged: boolean) => {
@ -154,6 +169,23 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
return disabled;
};
const selectCardMenuItems = filteredConditionalItems([
{
item: {
label: intl.formatMessage(messages.cancelOrder),
onSelect: onOrderCancel
},
shouldExist: canCancel
},
{
item: {
label: intl.formatMessage(messages.returnOrder),
onSelect: onOrderReturn
},
shouldExist: hasAnyItemsReplaceable(order)
}
]);
return (
<Form initial={initial} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => {
@ -169,19 +201,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
inline
title={<Title order={order} />}
>
{canCancel && (
<CardMenu
menuItems={[
{
label: intl.formatMessage({
defaultMessage: "Cancel order",
description: "button"
}),
onSelect: onOrderCancel
}
]}
/>
)}
<CardMenu menuItems={selectCardMenuItems} />
</PageHeader>
<div className={classes.date}>
{order && order.created ? (
@ -194,36 +214,26 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
</div>
<Grid>
<div>
{unfulfilled.length > 0 && (
<OrderUnfulfilledItems
<OrderUnfulfilledProductsCard
canFulfill={canFulfill}
lines={unfulfilled}
onFulfill={onOrderFulfill}
/>
)}
{renderCollection(
maybe(() => order.fulfillments),
(fulfillment, fulfillmentIndex) => (
<React.Fragment
key={maybe(() => fulfillment.id, "loading")}
>
{!(
unfulfilled.length === 0 && fulfillmentIndex === 0
) && <CardSpacer />}
<OrderFulfillment
{order?.fulfillments?.map(fulfillment => (
<React.Fragment key={fulfillment.id}>
<OrderFulfilledProductsCard
fulfillment={fulfillment}
orderNumber={maybe(() => order.number)}
orderNumber={order.number}
onOrderFulfillmentCancel={() =>
onFulfillmentCancel(fulfillment.id)
}
onTrackingCodeAdd={() =>
onFulfillmentTrackingNumberUpdate(fulfillment.id)
}
onRefund={onPaymentRefund}
/>
</React.Fragment>
)
)}
<CardSpacer />
))}
<OrderPayment
order={order}
onCapture={onPaymentCapture}
@ -281,5 +291,6 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
</Form>
);
};
OrderDetailsPage.displayName = "OrderDetailsPage";
export default OrderDetailsPage;

View file

@ -0,0 +1,44 @@
import { filteredConditionalItems } from "./utils";
describe("filteredConditionalItems", () => {
it("should return empty [] when no items has shouldExist set to true", () => {
const items = [
{
item: { id: "#1" },
shouldExist: false
},
{
item: { id: "#2" },
shouldExist: false
},
{
item: { id: "#3" },
shouldExist: false
}
];
expect(filteredConditionalItems(items)).toEqual([]);
});
it("should return only items that has shouldExist set to true", () => {
const items = [
{
item: { id: "#1" },
shouldExist: false
},
{
item: { id: "#2" },
shouldExist: true
},
{
item: { id: "#3" },
shouldExist: true
}
];
expect(filteredConditionalItems(items)).toEqual([
{ id: "#2" },
{ id: "#3" }
]);
});
});

View file

@ -0,0 +1,26 @@
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import {
getFulfilledFulfillemnts,
getUnfulfilledLines
} from "../OrderReturnPage/utils";
export const hasAnyItemsReplaceable = (order?: OrderDetails_order) => {
if (!order) {
return false;
}
const hasAnyUnfulfilledItems = getUnfulfilledLines(order).length > 0;
const hasAnyFulfilmentsToReturn = getFulfilledFulfillemnts(order).length > 0;
return hasAnyUnfulfilledItems || hasAnyFulfilmentsToReturn;
};
export interface ConditionalItem {
shouldExist: boolean;
item: any;
}
export const filteredConditionalItems = (items: ConditionalItem[]) =>
items.filter(({ shouldExist }) => shouldExist).map(({ item }) => item);

View file

@ -0,0 +1,64 @@
import { Button, CardActions } from "@material-ui/core";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import React from "react";
import { FormattedMessage } from "react-intl";
interface AcionButtonsProps {
status: FulfillmentStatus;
trackingNumber?: string;
onTrackingCodeAdd();
onRefund();
}
const statusesToShow = [
FulfillmentStatus.FULFILLED,
FulfillmentStatus.RETURNED
];
const ActionButtons: React.FC<AcionButtonsProps> = ({
status,
onTrackingCodeAdd,
trackingNumber,
onRefund
}) => {
const hasTrackingNumber = !!trackingNumber;
if (!statusesToShow.includes(status)) {
return null;
}
if (status === FulfillmentStatus.RETURNED) {
return (
<CardActions>
<Button color="primary" onClick={onRefund}>
<FormattedMessage
defaultMessage="Refund"
description="refund button"
/>
</Button>
</CardActions>
);
}
return hasTrackingNumber ? (
<CardActions>
<Button color="primary" onClick={onTrackingCodeAdd}>
<FormattedMessage
defaultMessage="Edit tracking"
description="edit tracking button"
/>
</Button>
</CardActions>
) : (
<CardActions>
<Button color="primary" onClick={onTrackingCodeAdd}>
<FormattedMessage
defaultMessage="Add tracking"
description="add tracking button"
/>
</Button>
</CardActions>
);
};
export default ActionButtons;

View file

@ -0,0 +1,103 @@
import { makeStyles, TableCell, TableRow, Typography } from "@material-ui/core";
import { getStringOrPlaceholder } from "@saleor/misc";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import classNames from "classnames";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import { OrderDetails_order_fulfillments } from "../../types/OrderDetails";
const useStyles = makeStyles(
theme => ({
infoLabel: {
display: "inline-block"
},
infoLabelWithMargin: {
marginBottom: theme.spacing()
},
infoRow: {
padding: theme.spacing(2, 3)
}
}),
{ name: "ExtraInfoLines" }
);
const messages = defineMessages({
fulfilled: {
defaultMessage: "Fulfilled from: ",
description: "fulfillment group"
},
restocked: {
defaultMessage: "Restocked from: ",
description: "restocked group"
},
tracking: {
defaultMessage: "Tracking Number: {trackingNumber}",
description: "tracking number"
}
});
const NUMBER_OF_COLUMNS = 5;
interface ExtraInfoLinesProps {
fulfillment?: OrderDetails_order_fulfillments;
}
const ExtraInfoLines: React.FC<ExtraInfoLinesProps> = ({ fulfillment }) => {
const intl = useIntl();
const classes = useStyles({});
if (!fulfillment || !fulfillment?.warehouse || !fulfillment?.trackingNumber) {
return null;
}
const { warehouse, trackingNumber, status } = fulfillment;
return (
<TableRow>
<TableCell className={classes.infoRow} colSpan={NUMBER_OF_COLUMNS}>
<Typography color="textSecondary" variant="body2">
{warehouse && (
<>
{intl.formatMessage(
status === FulfillmentStatus.RETURNED
? messages.restocked
: messages.fulfilled
)}
<Typography
className={classNames(classes.infoLabel, {
[classes.infoLabelWithMargin]: !!trackingNumber
})}
color="textPrimary"
variant="body2"
>
{getStringOrPlaceholder(warehouse?.name)}
</Typography>
</>
)}
</Typography>
<Typography color="textSecondary" variant="body2">
{trackingNumber && (
<FormattedMessage
defaultMessage="Tracking Number: {trackingNumber}"
values={{
trackingNumber: (
<Typography
className={classes.infoLabel}
color="textPrimary"
variant="body2"
>
{trackingNumber}
</Typography>
)
}}
/>
)}
</Typography>
</TableCell>
</TableRow>
);
};
export default ExtraInfoLines;

View file

@ -0,0 +1,114 @@
import Card from "@material-ui/core/Card";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import CardMenu from "@saleor/components/CardMenu";
import CardSpacer from "@saleor/components/CardSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { mergeRepeatedOrderLines } from "@saleor/orders/utils/data";
import React from "react";
import { useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc";
import { FulfillmentStatus } from "../../../types/globalTypes";
import { OrderDetails_order_fulfillments } from "../../types/OrderDetails";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import CardTitle from "../OrderReturnPage/OrderReturnRefundItemsCard/CardTitle";
import ActionButtons from "./ActionButtons";
import ExtraInfoLines from "./ExtraInfoLines";
const useStyles = makeStyles(
() => ({
table: {
tableLayout: "fixed"
}
}),
{ name: "OrderFulfillment" }
);
interface OrderFulfilledProductsCardProps {
fulfillment: OrderDetails_order_fulfillments;
orderNumber?: string;
onOrderFulfillmentCancel: () => void;
onTrackingCodeAdd: () => void;
onRefund: () => void;
}
const OrderFulfilledProductsCard: React.FC<OrderFulfilledProductsCardProps> = props => {
const {
fulfillment,
orderNumber,
onOrderFulfillmentCancel,
onTrackingCodeAdd,
onRefund
} = props;
const classes = useStyles(props);
const intl = useIntl();
if (!fulfillment) {
return null;
}
const getLines = () => {
const statusesToMergeLines = [
FulfillmentStatus.REFUNDED,
FulfillmentStatus.REFUNDED_AND_RETURNED,
FulfillmentStatus.RETURNED,
FulfillmentStatus.REPLACED
];
if (statusesToMergeLines.includes(fulfillment?.status)) {
return mergeRepeatedOrderLines(fulfillment.lines);
}
return fulfillment?.lines || [];
};
return (
<>
<Card>
<CardTitle
withStatus
lines={fulfillment?.lines}
fulfillmentOrder={fulfillment?.fulfillmentOrder}
status={fulfillment?.status}
orderNumber={orderNumber}
toolbar={
maybe(() => fulfillment.status) === FulfillmentStatus.FULFILLED && (
<CardMenu
menuItems={[
{
label: intl.formatMessage({
defaultMessage: "Cancel Fulfillment",
description: "button"
}),
onSelect: onOrderFulfillmentCancel
}
]}
/>
)
}
/>
<ResponsiveTable className={classes.table}>
<TableHeader />
<TableBody>
{renderCollection(getLines(), line => (
<TableLine line={line} />
))}
</TableBody>
<ExtraInfoLines fulfillment={fulfillment} />
</ResponsiveTable>
<ActionButtons
status={fulfillment?.status}
trackingNumber={fulfillment?.trackingNumber}
onTrackingCodeAdd={onTrackingCodeAdd}
onRefund={onRefund}
/>
</Card>
<CardSpacer />
</>
);
};
export default OrderFulfilledProductsCard;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderFulfilledProductsCard";
export * from "./OrderFulfilledProductsCard";

View file

@ -1,333 +0,0 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import CardMenu from "@saleor/components/CardMenu";
import CardTitle from "@saleor/components/CardTitle";
import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import { mergeRepeatedOrderLines } from "@saleor/orders/utils/data";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { getStringOrPlaceholder, maybe, renderCollection } from "../../../misc";
import { FulfillmentStatus } from "../../../types/globalTypes";
import { OrderDetails_order_fulfillments } from "../../types/OrderDetails";
const useStyles = makeStyles(
theme => ({
clickableRow: {
cursor: "pointer"
},
colName: {
width: "auto"
},
colNameLabel: {
marginLeft: AVATAR_MARGIN
},
colPrice: {
textAlign: "right",
width: 120
},
colQuantity: {
textAlign: "center",
width: 120
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 120
},
colTotal: {
textAlign: "right",
width: 120
},
infoLabel: {
display: "inline-block"
},
infoLabelWithMargin: {
marginBottom: theme.spacing()
},
infoRow: {
padding: theme.spacing(2, 3)
},
orderNumber: {
display: "inline",
marginLeft: theme.spacing(1)
},
statusBar: {
paddingTop: 0
},
table: {
tableLayout: "fixed"
}
}),
{ name: "OrderFulfillment" }
);
interface OrderFulfillmentProps {
fulfillment: OrderDetails_order_fulfillments;
orderNumber: string;
onOrderFulfillmentCancel: () => void;
onTrackingCodeAdd: () => void;
}
const numberOfColumns = 5;
const OrderFulfillment: React.FC<OrderFulfillmentProps> = props => {
const {
fulfillment,
orderNumber,
onOrderFulfillmentCancel,
onTrackingCodeAdd
} = props;
const classes = useStyles(props);
const intl = useIntl();
const lines =
fulfillment?.status === FulfillmentStatus.REFUNDED
? mergeRepeatedOrderLines(fulfillment?.lines)
: fulfillment?.lines;
const status = maybe(() => fulfillment.status);
const quantity = lines
? lines.map(line => line.quantity).reduce((prev, curr) => prev + curr, 0)
: "...";
return (
<Card>
<CardTitle
title={
!!lines ? (
<StatusLabel
label={
<>
{status === FulfillmentStatus.FULFILLED
? intl.formatMessage(
{
defaultMessage: "Fulfilled ({quantity})",
description: "section header"
},
{
quantity
}
)
: status === FulfillmentStatus.REFUNDED
? intl.formatMessage(
{
defaultMessage: "Refunded ({quantity})",
description: "refunded fulfillment, section header"
},
{
quantity
}
)
: intl.formatMessage(
{
defaultMessage: "Cancelled ({quantity})",
description: "cancelled fulfillment, section header"
},
{
quantity
}
)}
<Typography className={classes.orderNumber} variant="body1">
{maybe(
() => `#${orderNumber}-${fulfillment.fulfillmentOrder}`
)}
</Typography>
</>
}
status={
status === FulfillmentStatus.FULFILLED
? "success"
: status === FulfillmentStatus.REFUNDED
? "unspecified"
: "error"
}
/>
) : (
<Skeleton />
)
}
toolbar={
maybe(() => fulfillment.status) === FulfillmentStatus.FULFILLED && (
<CardMenu
menuItems={[
{
label: intl.formatMessage({
defaultMessage: "Cancel Fulfillment",
description: "button"
}),
onSelect: onOrderFulfillmentCancel
}
]}
/>
)
}
/>
<ResponsiveTable className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<span className={classes.colNameLabel}>
<FormattedMessage
defaultMessage="Product"
description="product name"
/>
</span>
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage
defaultMessage="SKU"
description="ordered product sku"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
defaultMessage="Quantity"
description="ordered product quantity"
/>
</TableCell>
<TableCell className={classes.colPrice}>
<FormattedMessage
defaultMessage="Price"
description="product price"
/>
</TableCell>
<TableCell className={classes.colTotal}>
<FormattedMessage
defaultMessage="Total"
description="order line total price"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(lines, line => (
<TableRow
className={!!line ? classes.clickableRow : undefined}
hover={!!line}
key={maybe(() => line.id)}
>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => line.orderLine.thumbnail.url)}
>
{maybe(() => line.orderLine.productName) || <Skeleton />}
</TableCellAvatar>
<TableCell className={classes.colSku}>
{line?.orderLine.productSku || <Skeleton />}
</TableCell>
<TableCell className={classes.colQuantity}>
{line?.quantity || <Skeleton />}
</TableCell>
<TableCell className={classes.colPrice}>
{maybe(() => line.orderLine.unitPrice.gross) ? (
<Money money={line.orderLine.unitPrice.gross} />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTotal}>
{maybe(
() => line.quantity * line.orderLine.unitPrice.gross.amount
) ? (
<Money
money={{
amount:
line.quantity * line.orderLine.unitPrice.gross.amount,
currency: line.orderLine.unitPrice.gross.currency
}}
/>
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
))}
{(fulfillment?.warehouse || fulfillment?.trackingNumber) && (
<TableRow>
<TableCell className={classes.infoRow} colSpan={numberOfColumns}>
<Typography color="textSecondary" variant="body2">
{fulfillment?.warehouse && (
<FormattedMessage
defaultMessage="Fulfilled from: {warehouseName}"
description="fulfillment group"
values={{
warehouseName: (
<Typography
className={classNames(classes.infoLabel, {
[classes.infoLabelWithMargin]: !!fulfillment?.trackingNumber
})}
color="textPrimary"
variant="body2"
>
{getStringOrPlaceholder(
fulfillment?.warehouse?.name
)}
</Typography>
)
}}
/>
)}
</Typography>
<Typography color="textSecondary" variant="body2">
{fulfillment?.trackingNumber && (
<FormattedMessage
defaultMessage="Tracking Number: {trackingNumber}"
values={{
trackingNumber: (
<Typography
className={classes.infoLabel}
color="textPrimary"
variant="body2"
>
{fulfillment.trackingNumber}
</Typography>
)
}}
/>
)}
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</ResponsiveTable>
{status === FulfillmentStatus.FULFILLED && !fulfillment.trackingNumber && (
<CardActions>
<Button color="primary" onClick={onTrackingCodeAdd}>
<FormattedMessage
defaultMessage="Add tracking"
description="fulfillment group tracking number"
/>
</Button>
</CardActions>
)}
{status === FulfillmentStatus.FULFILLED && fulfillment.trackingNumber && (
<CardActions>
<Button color="primary" onClick={onTrackingCodeAdd}>
<FormattedMessage
defaultMessage="Edit tracking"
description="fulfillment group tracking number"
/>
</Button>
</CardActions>
)}
</Card>
);
};
OrderFulfillment.displayName = "OrderFulfillment";
export default OrderFulfillment;

View file

@ -1,2 +0,0 @@
export { default } from "./OrderFulfillment";
export * from "./OrderFulfillment";

View file

@ -0,0 +1,211 @@
import { makeStyles, Typography } from "@material-ui/core";
import Money from "@saleor/components/Money";
import { TimelineEvent } from "@saleor/components/Timeline";
import { orderUrl } from "@saleor/orders/urls";
import { staffMemberDetailsUrl } from "@saleor/staff/urls";
import { OrderEventsEnum } from "@saleor/types/globalTypes";
import classNames from "classnames";
import camelCase from "lodash/camelCase";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import { OrderDetails_order_events } from "../../types/OrderDetails";
const useStyles = makeStyles(
theme => ({
eventSubtitle: {
marginBottom: theme.spacing(0.5),
marginTop: theme.spacing(1)
},
header: {
fontWeight: 500,
marginBottom: theme.spacing(1)
},
linesTableCell: {
paddingRight: theme.spacing(3)
},
root: { marginTop: theme.spacing(4) },
topSpacer: {
marginTop: theme.spacing(3)
},
user: {
marginBottom: theme.spacing(1)
}
}),
{ name: "OrderHistory" }
);
export const productTitles = defineMessages({
draftCreatedFromReplace: {
defaultMessage: "Products replaced",
description: "draft created from replace products list title",
id: "event products title draft reissued"
},
fulfillmentRefunded: {
defaultMessage: "Products refunded",
description: "refunded products list title",
id: "event products list title refunded"
},
fulfillmentReplaced: {
defaultMessage: "Products replaced",
description: "replaced products list title",
id: "event products list title replaced"
},
fulfillmentReturned: {
defaultMessage: "Products returned",
description: "returned products list title",
id: "event products list title returned"
}
});
export const titles = defineMessages({
draftCreatedFromReplace: {
defaultMessage: "Draft was reissued from order ",
description: "draft created from replace event title",
id: "event title draft reissued"
},
fulfillmentRefunded: {
defaultMessage: "Products were refunded by ",
description: "refunded event title",
id: "event title refunded"
},
fulfillmentReplaced: {
defaultMessage: "Products were replaced by ",
description: "replaced event title",
id: "event title replaced"
},
fulfillmentReturned: {
defaultMessage: "Products were returned by",
description: "returned event title",
id: "event title returned"
}
});
export const messages = defineMessages({
by: {
defaultMessage: "by",
description: "by preposition",
id: "by preposition"
},
refundedAmount: {
defaultMessage: "Refunded amount",
description: "amount title",
id: "amount title"
},
refundedShipment: {
defaultMessage: "Shipment was refunded",
description: "shipment refund title",
id: "shipment refund title"
}
});
interface ExtendedTimelineEventProps {
event: OrderDetails_order_events;
orderCurrency: string;
}
const ExtendedTimelineEvent: React.FC<ExtendedTimelineEventProps> = ({
event,
orderCurrency
}) => {
const {
id,
date,
type,
user,
lines,
amount,
shippingCostsIncluded,
relatedOrder
} = event;
const classes = useStyles({});
const intl = useIntl();
const eventTypeInCamelCase = camelCase(type);
const employeeName = `${user.firstName} ${user.lastName}`;
const titleElements = {
by: { text: intl.formatMessage(messages.by) },
employeeName: { link: staffMemberDetailsUrl(user.id), text: employeeName },
orderNumber: {
link: orderUrl(relatedOrder?.id),
text: `#${relatedOrder?.number}`
},
title: { text: intl.formatMessage(titles[eventTypeInCamelCase]) }
};
const selectTitleElements = () => {
const { title, by, employeeName, orderNumber } = titleElements;
switch (type) {
case OrderEventsEnum.DRAFT_CREATED_FROM_REPLACE: {
return [title, orderNumber, by, employeeName];
}
default: {
return [title, employeeName];
}
}
};
return (
<TimelineEvent date={date} titleElements={selectTitleElements()} key={id}>
{lines && (
<>
<Typography
variant="caption"
color="textSecondary"
className={classes.eventSubtitle}
>
{intl.formatMessage(productTitles[eventTypeInCamelCase])}
</Typography>
<table>
<tbody>
{lines.map(({ orderLine, quantity }) => (
<tr key={orderLine.id}>
<td className={classes.linesTableCell}>
{orderLine.productName}
</td>
<td className={classes.linesTableCell}>
<Typography variant="caption" color="textSecondary">
{orderLine.variantName}
</Typography>
</td>
<td className={classes.linesTableCell}>
<Typography variant="caption" color="textSecondary">
{`qty: ${quantity}`}
</Typography>
</td>
</tr>
))}
</tbody>
</table>
{(amount || amount === 0) && (
<>
<Typography
variant="caption"
color="textSecondary"
className={classNames(classes.eventSubtitle, classes.topSpacer)}
>
{intl.formatMessage(messages.refundedAmount)}
</Typography>
<Money
money={{
amount,
currency: orderCurrency
}}
/>
</>
)}
{shippingCostsIncluded && (
<Typography>
{intl.formatMessage(messages.refundedShipment)}
</Typography>
)}
</>
)}
</TimelineEvent>
);
};
export default ExtendedTimelineEvent;

View file

@ -2,28 +2,32 @@ import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Form from "@saleor/components/Form";
import Hr from "@saleor/components/Hr";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import {
Timeline,
TimelineAddNote,
TimelineEvent,
TimelineEventProps,
TimelineNote
} from "@saleor/components/Timeline";
import React from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
import { TitleElement } from "@saleor/components/Timeline/TimelineEventHeader";
import { OrderDetails_order_events } from "@saleor/orders/types/OrderDetails";
import { orderUrl } from "@saleor/orders/urls";
import {
OrderEventsEmailsEnum,
OrderEventsEnum
} from "../../../types/globalTypes";
import { OrderDetails_order_events } from "../../types/OrderDetails";
} from "@saleor/types/globalTypes";
import React from "react";
import { defineMessages } from "react-intl";
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
export interface FormData {
message: string;
}
import ExtendedTimelineEvent from "./ExtendedTimelineEvent";
import { getEventSecondaryTitle, isTimelineEventOfType } from "./utils";
const getEventMessage = (event: OrderDetails_order_events, intl: IntlShape) => {
export const getEventMessage = (
event: OrderDetails_order_events,
intl: IntlShape
) => {
switch (event.type) {
case OrderEventsEnum.CANCELED:
return intl.formatMessage({
@ -247,6 +251,21 @@ const getEventMessage = (event: OrderDetails_order_events, intl: IntlShape) => {
}
};
export const replacementCreatedMessages = defineMessages({
description: {
defaultMessage: "was created for replaced products",
description: "replacement created order history message description"
},
draftNumber: {
defaultMessage: "Draft #{orderNumber} ",
description: "replacement created order history message draft number"
}
});
export interface FormData {
message: string;
}
const useStyles = makeStyles(
theme => ({
eventSubtitle: {
@ -279,6 +298,45 @@ const OrderHistory: React.FC<OrderHistoryProps> = props => {
const intl = useIntl();
const getTimelineEventTitleProps = (
event: OrderDetails_order_events
): Partial<TimelineEventProps> => {
const { type, message } = event;
const title = isTimelineEventOfType("rawMessage", type)
? message
: getEventMessage(event, intl);
if (isTimelineEventOfType("secondaryTitle", type)) {
return {
secondaryTitle: intl.formatMessage(...getEventSecondaryTitle(event)),
title
};
}
return { title };
};
const getTitleElements = (
event: OrderDetails_order_events
): TitleElement[] => {
const { type, relatedOrder } = event;
switch (type) {
case OrderEventsEnum.ORDER_REPLACEMENT_CREATED: {
return [
{
link: orderUrl(relatedOrder?.id),
text: intl.formatMessage(replacementCreatedMessages.draftNumber, {
orderNumber: relatedOrder?.number
})
},
{ text: intl.formatMessage(replacementCreatedMessages.description) }
];
}
}
};
return (
<div className={classes.root}>
<Typography className={classes.header} color="textSecondary">
@ -301,106 +359,42 @@ const OrderHistory: React.FC<OrderHistoryProps> = props => {
.slice()
.reverse()
.map(event => {
if (event.type === OrderEventsEnum.NOTE_ADDED) {
const { id, user, date, message, type } = event;
if (isTimelineEventOfType("note", type)) {
return (
<TimelineNote
date={event.date}
user={event.user}
message={event.message}
key={event.id}
date={date}
user={user}
message={message}
key={id}
/>
);
}
if (event.type === OrderEventsEnum.ORDER_MARKED_AS_PAID) {
if (isTimelineEventOfType("extendable", type)) {
return (
<TimelineEvent
date={event.date}
title={getEventMessage(event, intl)}
secondaryTitle={intl.formatMessage(
{
defaultMessage:
"Transaction Reference {transactionReference}",
description: "transaction reference"
},
{ transactionReference: event.transactionReference }
)}
key={event.id}
<ExtendedTimelineEvent
event={event}
orderCurrency={orderCurrency}
/>
);
}
if (event.type === OrderEventsEnum.FULFILLMENT_REFUNDED) {
if (isTimelineEventOfType("linked", type)) {
return (
<TimelineEvent
date={event.date}
title={getEventMessage(event, intl)}
key={event.id}
>
{event.lines && (
<>
<Typography
variant="caption"
color="textSecondary"
className={classes.eventSubtitle}
>
<FormattedMessage defaultMessage="Products refunded" />
</Typography>
<table>
<tbody>
{event.lines.map(line => (
<tr key={line.orderLine.id}>
<td className={classes.linesTableCell}>
{line.orderLine.productName}
</td>
<td className={classes.linesTableCell}>
<Typography
variant="caption"
color="textSecondary"
>
{line.orderLine.variantName}
</Typography>
</td>
<td className={classes.linesTableCell}>
<Typography
variant="caption"
color="textSecondary"
>
{`qty: ${line.quantity}`}
</Typography>
</td>
</tr>
))}
</tbody>
</table>
<Typography
variant="caption"
color="textSecondary"
className={classes.eventSubtitle}
>
<FormattedMessage defaultMessage="Refunded amount" />
</Typography>
{(event.amount || event.amount === 0) && (
<Money
money={{
amount: event.amount,
currency: orderCurrency
}}
titleElements={getTitleElements(event)}
key={id}
date={date}
/>
)}
{event.shippingCostsIncluded && (
<Typography>
<FormattedMessage defaultMessage="Shipment was refunded" />
</Typography>
)}
</>
)}
</TimelineEvent>
);
}
return (
<TimelineEvent
date={event.date}
title={getEventMessage(event, intl)}
key={event.id}
{...getTimelineEventTitleProps(event)}
key={id}
date={date}
/>
);
})}

View file

@ -0,0 +1,41 @@
import { OrderDetails_order_events } from "@saleor/orders/types/OrderDetails";
import { OrderEventsEnum } from "@saleor/types/globalTypes";
import { MessageDescriptor } from "react-intl";
export const getEventSecondaryTitle = (
event: OrderDetails_order_events
): [MessageDescriptor, any?] => {
switch (event.type) {
case OrderEventsEnum.ORDER_MARKED_AS_PAID: {
return [
{
defaultMessage: "Transaction Reference {transactionReference}",
description: "transaction reference",
id: "transaction-reference-order-history"
},
{ transactionReference: event.transactionReference }
];
}
}
};
const timelineEventTypes = {
extendable: [
OrderEventsEnum.FULFILLMENT_REFUNDED,
OrderEventsEnum.FULFILLMENT_REPLACED,
OrderEventsEnum.FULFILLMENT_RETURNED,
OrderEventsEnum.DRAFT_CREATED_FROM_REPLACE
],
linked: [OrderEventsEnum.ORDER_REPLACEMENT_CREATED],
note: [OrderEventsEnum.NOTE_ADDED],
rawMessage: [
OrderEventsEnum.OTHER,
OrderEventsEnum.EXTERNAL_SERVICE_NOTIFICATION
],
secondaryTitle: [OrderEventsEnum.ORDER_MARKED_AS_PAID]
};
export const isTimelineEventOfType = (
type: "extendable" | "secondaryTitle" | "rawMessage" | "note" | "linked",
eventType: OrderEventsEnum
) => !!timelineEventTypes[type]?.includes(eventType);

View file

@ -0,0 +1,95 @@
import { makeStyles, TableCell, TableHead, TableRow } from "@material-ui/core";
import React from "react";
import { FormattedMessage } from "react-intl";
const useStyles = makeStyles(
theme => ({
clickableRow: {
cursor: "pointer"
},
colName: {
textAlign: "left",
width: "auto"
},
colPrice: {
textAlign: "right",
width: 120
},
colQuantity: {
textAlign: "center",
width: 120
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 120
},
colTotal: {
textAlign: "right",
width: 120
},
infoLabel: {
display: "inline-block"
},
infoLabelWithMargin: {
marginBottom: theme.spacing()
},
infoRow: {
padding: theme.spacing(2, 3)
},
orderNumber: {
display: "inline",
marginLeft: theme.spacing(1)
},
statusBar: {
paddingTop: 0
},
table: {
tableLayout: "fixed"
}
}),
{ name: "TableHeader" }
);
const TableHeader = () => {
const classes = useStyles({});
return (
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage
defaultMessage="Product"
description="product name"
/>
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage
defaultMessage="SKU"
description="ordered product sku"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
defaultMessage="Quantity"
description="ordered product quantity"
/>
</TableCell>
<TableCell className={classes.colPrice}>
<FormattedMessage
defaultMessage="Price"
description="product price"
/>
</TableCell>
<TableCell className={classes.colTotal}>
<FormattedMessage
defaultMessage="Total"
description="order line total price"
/>
</TableCell>
</TableRow>
</TableHead>
);
};
export default TableHeader;

View file

@ -0,0 +1,129 @@
import { makeStyles, TableCell, TableRow } from "@material-ui/core";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import { maybe } from "@saleor/misc";
import {
OrderDetails_order_fulfillments_lines,
OrderDetails_order_lines
} from "@saleor/orders/types/OrderDetails";
import React from "react";
const useStyles = makeStyles(
theme => ({
clickableRow: {
cursor: "pointer"
},
colName: {
width: "auto"
},
colNameLabel: {
marginLeft: AVATAR_MARGIN
},
colPrice: {
textAlign: "right",
width: 120
},
colQuantity: {
textAlign: "center",
width: 120
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 120
},
colTotal: {
textAlign: "right",
width: 120
},
infoLabel: {
display: "inline-block"
},
infoLabelWithMargin: {
marginBottom: theme.spacing()
},
infoRow: {
padding: theme.spacing(2, 3)
},
orderNumber: {
display: "inline",
marginLeft: theme.spacing(1)
},
statusBar: {
paddingTop: 0
},
table: {
tableLayout: "fixed"
}
}),
{ name: "TableLine" }
);
interface TableLineProps {
line: OrderDetails_order_fulfillments_lines | OrderDetails_order_lines;
isOrderLine?: boolean;
}
const TableLine: React.FC<TableLineProps> = ({
line: lineData,
isOrderLine = false
}) => {
const classes = useStyles({});
const { quantity, quantityFulfilled } = lineData as OrderDetails_order_lines;
if (!lineData) {
return <Skeleton />;
}
const line = isOrderLine
? ({
...lineData,
orderLine: lineData
} as OrderDetails_order_fulfillments_lines)
: (lineData as OrderDetails_order_fulfillments_lines);
const quantityToDisplay = isOrderLine
? quantity - quantityFulfilled
: quantity;
return (
<TableRow className={classes.clickableRow} hover key={line.id}>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => line.orderLine.thumbnail.url)}
>
{maybe(() => line.orderLine.productName) || <Skeleton />}
</TableCellAvatar>
<TableCell className={classes.colSku}>
{line?.orderLine.productSku || <Skeleton />}
</TableCell>
<TableCell className={classes.colQuantity}>
{quantityToDisplay || <Skeleton />}
</TableCell>
<TableCell className={classes.colPrice}>
{maybe(() => line.orderLine.unitPrice.gross) ? (
<Money money={line.orderLine.unitPrice.gross} />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTotal}>
{maybe(() => line.quantity * line.orderLine.unitPrice.gross.amount) ? (
<Money
money={{
amount: line.quantity * line.orderLine.unitPrice.gross.amount,
currency: line.orderLine.unitPrice.gross.currency
}}
/>
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
};
export default TableLine;

View file

@ -1,2 +0,0 @@
export * from "./OrderRefundAmount";
export { default } from "./OrderRefundAmount";

View file

@ -1,153 +0,0 @@
import { makeStyles } from "@material-ui/core";
import Money, { IMoney } from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import React from "react";
import { FormattedMessage } from "react-intl";
const useStyles = makeStyles(
theme => ({
highlightedRow: {
fontWeight: 600
},
root: {
...theme.typography.body1,
lineHeight: 1.9,
width: "100%"
},
textRight: {
minWidth: 30,
textAlign: "right"
}
}),
{ name: "OrderRefundAmountValues" }
);
export interface OrderRefundAmountValuesProps {
authorizedAmount: IMoney;
shipmentCost?: IMoney;
selectedProductsValue?: IMoney;
previouslyRefunded: IMoney;
maxRefund: IMoney;
proposedRefundAmount?: IMoney;
refundTotalAmount?: IMoney;
}
const OrderRefundAmountValues: React.FC<OrderRefundAmountValuesProps> = ({
authorizedAmount,
shipmentCost,
selectedProductsValue,
previouslyRefunded,
maxRefund,
proposedRefundAmount,
refundTotalAmount
}) => {
const classes = useStyles({});
return (
<table className={classes.root}>
<tbody>
<tr>
<td>
<FormattedMessage
defaultMessage="Authorized Amount"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
{authorizedAmount?.amount !== undefined ? (
<Money money={authorizedAmount} />
) : (
<Skeleton />
)}
</td>
</tr>
{shipmentCost?.amount !== undefined && (
<tr>
<td>
<FormattedMessage
defaultMessage="Shipment cost"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
<Money money={shipmentCost} />
</td>
</tr>
)}
{selectedProductsValue?.amount !== undefined && (
<tr>
<td>
<FormattedMessage
defaultMessage="Selected products value"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
<Money money={selectedProductsValue} />
</td>
</tr>
)}
<tr>
<td>
<FormattedMessage
defaultMessage="Previously refunded"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
{previouslyRefunded?.amount !== undefined ? (
<>
<Money money={previouslyRefunded} />
</>
) : (
<Skeleton />
)}
</td>
</tr>
<tr className={classes.highlightedRow}>
<td>
<FormattedMessage
defaultMessage="Max Refund"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
{maxRefund?.amount !== undefined ? (
<Money money={maxRefund} />
) : (
<Skeleton />
)}
</td>
</tr>
{proposedRefundAmount?.amount !== undefined && (
<tr className={classes.highlightedRow}>
<td>
<FormattedMessage
defaultMessage="Proposed refund amount"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
<Money money={proposedRefundAmount} />
</td>
</tr>
)}
{refundTotalAmount?.amount !== undefined && (
<tr className={classes.highlightedRow}>
<td>
<FormattedMessage
defaultMessage="Refund total amount"
description="order refund amount"
/>
</td>
<td className={classes.textRight}>
<Money money={refundTotalAmount} />
</td>
</tr>
)}
</tbody>
</table>
);
};
OrderRefundAmountValues.displayName = "OrderRefundAmountValues";
export default OrderRefundAmountValues;

View file

@ -1,2 +0,0 @@
export * from "./OrderRefundAmountValues";
export { default } from "./OrderRefundAmountValues";

View file

@ -12,8 +12,12 @@ import React from "react";
import { useIntl } from "react-intl";
import OrderRefund from "../OrderRefund";
import OrderRefundAmount from "../OrderRefundAmount";
import OrderRefundFulfilledProducts from "../OrderRefundFulfilledProducts";
import OrderRefundAmount from "../OrderRefundReturnAmount";
import {
getMiscellaneousAmountValues,
getRefundProductsAmountValues
} from "../OrderRefundReturnAmount/utils";
import OrderRefundUnfulfilledProducts from "../OrderRefundUnfulfilledProducts";
import OrderRefundForm, {
OrderRefundSubmitData,
@ -55,7 +59,10 @@ const OrderRefundPage: React.FC<OrderRefundPageProps> = props => {
defaultType={defaultType}
onSubmit={onSubmit}
>
{({ data, handlers, change, submit }) => (
{({ data, handlers, change, submit }) => {
const isProductRefund = data.type === OrderRefundType.PRODUCTS;
return (
<Container>
<AppHeader onBack={onBack}>
{order?.number
@ -86,8 +93,12 @@ const OrderRefundPage: React.FC<OrderRefundPageProps> = props => {
/>
<Grid>
<div>
<OrderRefund data={data} disabled={disabled} onChange={change} />
{data.type === OrderRefundType.PRODUCTS && (
<OrderRefund
data={data}
disabled={disabled}
onChange={change}
/>
{isProductRefund && (
<>
{unfulfilledLines?.length > 0 && (
<>
@ -129,6 +140,11 @@ const OrderRefundPage: React.FC<OrderRefundPageProps> = props => {
</div>
<div>
<OrderRefundAmount
amountData={
isProductRefund
? getRefundProductsAmountValues(order, data)
: getMiscellaneousAmountValues(order)
}
data={data}
order={order}
disabled={disabled}
@ -139,7 +155,8 @@ const OrderRefundPage: React.FC<OrderRefundPageProps> = props => {
</div>
</Grid>
</Container>
)}
);
}}
</OrderRefundForm>
);
};

View file

@ -9,91 +9,22 @@ import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Hr from "@saleor/components/Hr";
import { IMoney } from "@saleor/components/Money";
import PriceField from "@saleor/components/PriceField";
import { OrderErrorFragment } from "@saleor/fragments/types/OrderErrorFragment";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import { OrderRefundData_order } from "@saleor/orders/types/OrderRefundData";
import {
getAllFulfillmentLinesPriceSum,
getPreviouslyRefundedPrice,
getRefundedLinesPriceSum
} from "@saleor/orders/utils/data";
import { getFormErrors } from "@saleor/utils/errors";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import OrderRefundAmountValues, {
OrderRefundAmountValuesProps
} from "../OrderRefundAmountValues";
import {
OrderRefundAmountCalculationMode,
OrderRefundFormData,
OrderRefundType
} from "../OrderRefundPage/form";
const getMiscellaneousAmountValues = (
order: OrderRefundData_order
): OrderRefundAmountValuesProps => {
const authorizedAmount = order?.total?.gross;
const previouslyRefunded = getPreviouslyRefundedPrice(order);
const maxRefund = order?.totalCaptured;
return {
authorizedAmount,
maxRefund,
previouslyRefunded
};
};
const getProductsAmountValues = (
order: OrderRefundData_order,
data: OrderRefundFormData
): OrderRefundAmountValuesProps => {
const authorizedAmount = order?.total?.gross;
const shipmentCost =
authorizedAmount?.currency &&
(order?.shippingPrice?.gross || {
amount: 0,
currency: authorizedAmount?.currency
});
const previouslyRefunded = getPreviouslyRefundedPrice(order);
const maxRefund = order?.totalCaptured;
const refundedLinesSum = getRefundedLinesPriceSum(
order?.lines,
data.refundedProductQuantities
);
const allFulfillmentLinesSum = getAllFulfillmentLinesPriceSum(
order?.fulfillments,
data.refundedFulfilledProductQuantities
);
const allLinesSum = refundedLinesSum + allFulfillmentLinesSum;
const calculatedTotalAmount = data.refundShipmentCosts
? allLinesSum + shipmentCost?.amount
: allLinesSum;
const selectedProductsValue = authorizedAmount?.currency && {
amount: allLinesSum,
currency: authorizedAmount.currency
};
const proposedRefundAmount = authorizedAmount?.currency && {
amount: calculatedTotalAmount,
currency: authorizedAmount.currency
};
const refundTotalAmount = authorizedAmount?.currency && {
amount: calculatedTotalAmount,
currency: authorizedAmount.currency
};
return {
authorizedAmount,
maxRefund,
previouslyRefunded,
proposedRefundAmount,
refundTotalAmount,
selectedProductsValue,
shipmentCost
};
};
import { OrderReturnFormData } from "../OrderReturnPage/form";
import OrderRefundAmountValues, {
OrderRefundAmountValuesProps
} from "./OrderRefundReturnAmountValues";
import RefundAmountInput from "./RefundAmountInput";
const useStyles = makeStyles(
theme => ({
@ -124,80 +55,54 @@ const useStyles = makeStyles(
{ name: "OrderRefundAmount" }
);
interface RefundAmountInputProps {
data: OrderRefundFormData;
maxRefund: IMoney;
currencySymbol: string;
amountTooSmall: boolean;
amountTooBig: boolean;
disabled: boolean;
errors: OrderErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
}
const RefundAmountInput: React.FC<RefundAmountInputProps> = props => {
const {
data,
maxRefund,
amountTooSmall,
amountTooBig,
currencySymbol,
disabled,
errors,
onChange
} = props;
const intl = useIntl();
const classes = useStyles(props);
const formErrors = getFormErrors(["amount"], errors);
return (
<PriceField
disabled={disabled}
onChange={onChange}
currencySymbol={currencySymbol}
name={"amount" as keyof FormData}
value={data.amount}
label={intl.formatMessage({
defaultMessage: "Amount",
description: "order refund amount, input label"
})}
className={classes.priceField}
InputProps={{ inputProps: { max: maxRefund?.amount } }}
inputProps={{
"data-test": "amountInput",
max: maxRefund?.amount
}}
error={!!formErrors.amount || amountTooSmall || amountTooBig}
hint={
getOrderErrorMessage(formErrors.amount, intl) ||
(amountTooSmall &&
intl.formatMessage({
defaultMessage: "Amount must be bigger than 0"
})) ||
(amountTooBig &&
intl.formatMessage({
defaultMessage: "Amount cannot be bigger than max refund"
}))
const messages = defineMessages({
refundButton: {
defaultMessage: "Refund",
description: "order refund amount button"
},
refundCannotBeFulfilled: {
defaultMessage: "Refunded items can't be fulfilled",
description: "order refund subtitle"
},
returnButton: {
defaultMessage: "Return & Replace products",
description: "order return amount button"
},
returnCannotBeFulfilled: {
defaultMessage: "Returned items can't be fulfilled",
description: "order return subtitle"
}
/>
);
};
});
interface OrderRefundAmountProps {
data: OrderRefundFormData;
order: OrderRefundData_order;
data: OrderRefundFormData | OrderReturnFormData;
order: OrderRefundData_order | OrderDetails_order;
disabled: boolean;
disableSubmitButton?: boolean;
isReturn?: boolean;
errors: OrderErrorFragment[];
amountData: OrderRefundAmountValuesProps;
onChange: (event: React.ChangeEvent<any>) => void;
onRefund: () => void;
}
const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
const { data, order, disabled, errors, onChange, onRefund } = props;
const {
data,
order,
disabled,
errors,
onChange,
onRefund,
isReturn = false,
amountData,
disableSubmitButton
} = props;
const classes = useStyles(props);
const intl = useIntl();
const { type = OrderRefundType.PRODUCTS } = data as OrderRefundFormData;
const amountCurrency = order?.total?.gross?.currency;
const {
@ -207,14 +112,12 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
proposedRefundAmount,
refundTotalAmount,
selectedProductsValue,
shipmentCost
} =
data.type === OrderRefundType.PRODUCTS
? getProductsAmountValues(order, data)
: getMiscellaneousAmountValues(order);
shipmentCost,
replacedProductsValue
} = amountData;
const selectedRefundAmount =
data.type === OrderRefundType.PRODUCTS &&
type === OrderRefundType.PRODUCTS &&
data.amountCalculationMode === OrderRefundAmountCalculationMode.AUTOMATIC
? refundTotalAmount?.amount
: data.amount;
@ -222,8 +125,9 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
const isAmountTooSmall = selectedRefundAmount && selectedRefundAmount <= 0;
const isAmountTooBig = selectedRefundAmount > maxRefund?.amount;
const disableRefundButton =
!selectedRefundAmount || isAmountTooSmall || isAmountTooBig;
const disableRefundButton = isReturn
? disableSubmitButton || isAmountTooSmall || isAmountTooBig
: !selectedRefundAmount || isAmountTooBig || isAmountTooSmall;
return (
<Card>
@ -234,7 +138,7 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
})}
/>
<CardContent>
{data.type === OrderRefundType.PRODUCTS && (
{type === OrderRefundType.PRODUCTS && (
<RadioGroup
value={data.amountCalculationMode}
onChange={onChange}
@ -261,6 +165,7 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
name="refundShipmentCosts"
onChange={onChange}
/>
<CardSpacer />
<OrderRefundAmountValues
authorizedAmount={authorizedAmount}
previouslyRefunded={previouslyRefunded}
@ -268,8 +173,8 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
selectedProductsValue={selectedProductsValue}
refundTotalAmount={refundTotalAmount}
shipmentCost={data.refundShipmentCosts && shipmentCost}
replacedProductsValue={replacedProductsValue}
/>
<CardSpacer />
</>
)}
<Hr className={classes.hr} />
@ -302,9 +207,10 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
selectedProductsValue={selectedProductsValue}
proposedRefundAmount={proposedRefundAmount}
shipmentCost={data.refundShipmentCosts && shipmentCost}
replacedProductsValue={replacedProductsValue}
/>
<RefundAmountInput
data={data}
data={data as OrderRefundFormData}
maxRefund={maxRefund}
amountTooSmall={isAmountTooSmall}
amountTooBig={isAmountTooBig}
@ -317,7 +223,7 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
)}
</RadioGroup>
)}
{data.type === OrderRefundType.MISCELLANEOUS && (
{type === OrderRefundType.MISCELLANEOUS && (
<>
<OrderRefundAmountValues
authorizedAmount={authorizedAmount}
@ -325,7 +231,7 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
maxRefund={maxRefund}
/>
<RefundAmountInput
data={data}
data={data as OrderRefundFormData}
maxRefund={maxRefund}
amountTooSmall={isAmountTooSmall}
amountTooBig={isAmountTooBig}
@ -337,17 +243,16 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
</>
)}
<Button
type="submit"
color="primary"
variant="contained"
fullWidth
size="large"
onClick={onRefund}
className={classes.refundButton}
disabled={disabled || disableRefundButton}
disabled={disableRefundButton}
data-test="submit"
>
{!disableRefundButton ? (
{!disableRefundButton && !isReturn ? (
<FormattedMessage
defaultMessage="Refund {currency} {amount}"
description="order refund amount, input button"
@ -357,10 +262,9 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
}}
/>
) : (
<FormattedMessage
defaultMessage="Refund"
description="order refund amount, input button"
/>
intl.formatMessage(
isReturn ? messages.returnButton : messages.refundButton
)
)}
</Button>
<Typography
@ -368,10 +272,11 @@ const OrderRefundAmount: React.FC<OrderRefundAmountProps> = props => {
color="textSecondary"
className={classes.refundCaution}
>
<FormattedMessage
defaultMessage="Refunded items cant be fulfilled"
description="order refund amount"
/>
{intl.formatMessage(
isReturn
? messages.returnCannotBeFulfilled
: messages.refundCannotBeFulfilled
)}
</Typography>
</CardContent>
</Card>

View file

@ -0,0 +1,133 @@
import { makeStyles } from "@material-ui/core";
import Money, { IMoney } from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import classNames from "classnames";
import { reduce } from "lodash";
import React from "react";
import { useIntl } from "react-intl";
import { defineMessages } from "react-intl";
const useStyles = makeStyles(
theme => ({
container: {
...theme.typography.body1,
lineHeight: 1.9,
width: "100%"
},
highlightedRow: {
fontWeight: 600
},
row: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
marginBottom: theme.spacing(2),
textAlign: "right"
}
}),
{ name: "OrderRefundAmountValues" }
);
export interface OrderRefundAmountValuesProps {
authorizedAmount: IMoney;
shipmentCost?: IMoney;
selectedProductsValue?: IMoney;
previouslyRefunded: IMoney;
maxRefund: IMoney;
proposedRefundAmount?: IMoney;
replacedProductsValue?: IMoney;
refundTotalAmount?: IMoney;
}
const messages = defineMessages({
authorizedAmount: {
defaultMessage: "Authorized Amount",
description: "order refund amount"
},
maxRefund: {
defaultMessage: "Max Refund",
description: "order refund amount"
},
previouslyRefunded: {
defaultMessage: "Previously refunded",
description: "order refund amount"
},
proposedRefundAmount: {
defaultMessage: "Proposed refund amount",
description: "order refund amount"
},
refundTotalAmount: {
defaultMessage: "Refund total amount",
description: "order refund amount"
},
replacedProductsValue: {
defaultMessage: "Replaced Products Value",
description: "order refund amount"
},
selectedProductsValue: {
defaultMessage: "Selected Products Value",
description: "order refund amount"
},
shipmentCost: {
defaultMessage: "Shipment Cost",
description: "order refund amount"
}
});
const OrderRefundAmountValues: React.FC<OrderRefundAmountValuesProps> = props => {
const intl = useIntl();
const classes = useStyles({});
const orderedKeys: Array<keyof OrderRefundAmountValuesProps> = [
"authorizedAmount",
"shipmentCost",
"selectedProductsValue",
"previouslyRefunded",
"replacedProductsValue",
"maxRefund",
"refundTotalAmount"
];
const highlightedItems: Array<keyof OrderRefundAmountValuesProps> = [
"maxRefund",
"refundTotalAmount"
];
const items = reduce(
orderedKeys,
(result, key) => {
const value = props[key];
if (!value) {
return result;
}
return [
...result,
{ data: value, highlighted: highlightedItems.includes(key), key }
];
},
[]
);
return (
<div className={classes.container}>
{items.map(({ key, data, highlighted }) => (
<div
className={classNames(classes.row, {
[classes.highlightedRow]: highlighted
})}
key={key}
>
{intl.formatMessage(messages[key])}
<div>
{data?.amount !== undefined ? <Money money={data} /> : <Skeleton />}
</div>
</div>
))}
</div>
);
};
OrderRefundAmountValues.displayName = "OrderRefundAmountValues";
export default OrderRefundAmountValues;

View file

@ -0,0 +1,108 @@
import { makeStyles } from "@material-ui/core";
import { IMoney } from "@saleor/components/Money";
import PriceField from "@saleor/components/PriceField";
import { OrderErrorFragment } from "@saleor/fragments/types/OrderErrorFragment";
import { getFormErrors } from "@saleor/utils/errors";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import { OrderRefundFormData } from "../OrderRefundPage/form";
const useStyles = makeStyles(
theme => ({
hr: {
margin: theme.spacing(1, 0)
},
maxRefundRow: {
fontWeight: 600
},
priceField: {
marginTop: theme.spacing(2)
},
refundButton: {
marginTop: theme.spacing(2)
},
refundCaution: {
marginTop: theme.spacing(1)
},
root: {
...theme.typography.body1,
lineHeight: 1.9,
width: "100%"
},
textRight: {
textAlign: "right"
}
}),
{ name: "OrderRefundAmount" }
);
interface RefundAmountInputProps {
data: OrderRefundFormData;
maxRefund: IMoney;
currencySymbol: string;
amountTooSmall: boolean;
amountTooBig: boolean;
disabled: boolean;
errors: OrderErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
}
const messages = defineMessages({
amountTooBig: {
defaultMessage: "Amount cannot be bigger than max refund",
description: "Amount error message"
},
amountTooSmall: {
defaultMessage: "Amount must be bigger than 0",
description: "Amount error message"
},
label: {
defaultMessage: "Amount",
description: "order refund amount, input label"
}
});
const RefundAmountInput: React.FC<RefundAmountInputProps> = props => {
const {
data,
maxRefund,
amountTooSmall,
amountTooBig,
currencySymbol,
disabled,
errors,
onChange
} = props;
const intl = useIntl();
const classes = useStyles(props);
const formErrors = getFormErrors(["amount"], errors);
const isError = !!formErrors.amount || amountTooSmall || amountTooBig;
return (
<PriceField
disabled={disabled}
onChange={onChange}
currencySymbol={currencySymbol}
name={"amount" as keyof FormData}
value={data.amount}
label={intl.formatMessage(messages.label)}
className={classes.priceField}
InputProps={{ inputProps: { max: maxRefund?.amount } }}
inputProps={{
"data-test": "amountInput",
max: maxRefund?.amount
}}
error={isError}
hint={
getOrderErrorMessage(formErrors.amount, intl) ||
(amountTooSmall && intl.formatMessage(messages.amountTooSmall)) ||
(amountTooBig && intl.formatMessage(messages.amountTooBig))
}
/>
);
};
export default RefundAmountInput;

View file

@ -0,0 +1,2 @@
export * from "./OrderRefundReturnAmount";
export { default } from "./OrderRefundReturnAmount";

View file

@ -0,0 +1,203 @@
import { IMoney } from "@saleor/components/Money";
import { FormsetData } from "@saleor/hooks/useFormset";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import { OrderRefundData_order } from "@saleor/orders/types/OrderRefundData";
import {
getAllFulfillmentLinesPriceSum,
getPreviouslyRefundedPrice,
getRefundedLinesPriceSum,
getReplacedProductsAmount,
getReturnSelectedProductsAmount
} from "@saleor/orders/utils/data";
import { OrderRefundFormData } from "../OrderRefundPage/form";
import { LineItemData, OrderReturnFormData } from "../OrderReturnPage/form";
import { OrderRefundAmountValuesProps } from "./OrderRefundReturnAmountValues";
export const getMiscellaneousAmountValues = (
order: OrderRefundData_order
): OrderRefundAmountValuesProps => {
const authorizedAmount = order?.total?.gross;
const previouslyRefunded = getPreviouslyRefundedPrice(order);
const maxRefund = order?.totalCaptured;
return {
authorizedAmount,
maxRefund,
previouslyRefunded
};
};
const getAuthorizedAmount = (order: OrderRefundData_order) =>
order?.total?.gross;
const getShipmentCost = (order: OrderRefundData_order) =>
getAuthorizedAmount(order)?.currency &&
(order?.shippingPrice?.gross || {
amount: 0,
currency: getAuthorizedAmount(order)?.currency
});
const getMaxRefund = (order: OrderRefundData_order) => order?.totalCaptured;
export const getProductsAmountValues = (
order: OrderRefundData_order,
fulfilledItemsQuantities: FormsetData<null | LineItemData, string | number>,
unfulfilledItemsQuantities: FormsetData<null | LineItemData, string | number>,
shipmentCosts
): OrderRefundAmountValuesProps => {
const authorizedAmount = getAuthorizedAmount(order);
const shipmentCost = getShipmentCost(order);
const previouslyRefunded = getPreviouslyRefundedPrice(order);
const maxRefund = getMaxRefund(order);
const refundedLinesSum = getRefundedLinesPriceSum(
order?.lines,
unfulfilledItemsQuantities as FormsetData<null, string>
);
const allFulfillmentLinesSum = getAllFulfillmentLinesPriceSum(
order?.fulfillments,
fulfilledItemsQuantities as FormsetData<null, string>
);
const allLinesSum = refundedLinesSum + allFulfillmentLinesSum;
const calculatedTotalAmount = getCalculatedTotalAmount({
allLinesSum,
maxRefund,
previouslyRefunded,
shipmentCost,
shipmentCosts
});
const selectedProductsValue = authorizedAmount?.currency && {
amount: allLinesSum,
currency: authorizedAmount.currency
};
const proposedRefundAmount = authorizedAmount?.currency && {
amount: calculatedTotalAmount,
currency: authorizedAmount.currency
};
const refundTotalAmount = authorizedAmount?.currency && {
amount: calculatedTotalAmount,
currency: authorizedAmount.currency
};
return {
authorizedAmount,
maxRefund,
previouslyRefunded,
proposedRefundAmount,
refundTotalAmount,
selectedProductsValue,
shipmentCost
};
};
const getCalculatedTotalAmount = ({
shipmentCost,
shipmentCosts,
allLinesSum,
maxRefund
}: {
shipmentCost: IMoney;
shipmentCosts: IMoney;
allLinesSum: number;
previouslyRefunded: IMoney;
maxRefund: IMoney;
}) => {
if (maxRefund?.amount === 0) {
return 0;
}
const shipmentCostValue = shipmentCost ? shipmentCost.amount : 0;
const calculatedTotalAmount = shipmentCosts
? allLinesSum + shipmentCostValue
: allLinesSum;
return calculatedTotalAmount;
};
const getReturnTotalAmount = ({
selectedProductsValue,
refundShipmentCosts,
order,
maxRefund
}: {
order: OrderDetails_order;
selectedProductsValue: IMoney;
refundShipmentCosts: boolean;
maxRefund: IMoney;
}) => {
if (maxRefund?.amount === 0) {
return 0;
}
if (refundShipmentCosts) {
const totalValue =
selectedProductsValue?.amount + getShipmentCost(order)?.amount;
return totalValue || 0;
}
return selectedProductsValue?.amount || 0;
};
export const getReturnProductsAmountValues = (
order: OrderDetails_order,
formData: OrderReturnFormData
) => {
const authorizedAmount = getAuthorizedAmount(order);
const {
fulfiledItemsQuantities,
unfulfiledItemsQuantities,
refundShipmentCosts
} = formData;
const replacedProductsValue = authorizedAmount?.currency && {
amount: getReplacedProductsAmount(order, formData),
currency: authorizedAmount.currency
};
const selectedProductsValue = authorizedAmount?.currency && {
amount: getReturnSelectedProductsAmount(order, formData),
currency: authorizedAmount.currency
};
const refundTotalAmount = authorizedAmount?.currency && {
amount: getReturnTotalAmount({
maxRefund: getMaxRefund(order),
order,
refundShipmentCosts,
selectedProductsValue
}),
currency: authorizedAmount.currency
};
return {
...getProductsAmountValues(
order,
fulfiledItemsQuantities,
unfulfiledItemsQuantities,
refundShipmentCosts
),
refundTotalAmount,
replacedProductsValue,
selectedProductsValue
};
};
export const getRefundProductsAmountValues = (
order: OrderRefundData_order,
{
refundedFulfilledProductQuantities,
refundShipmentCosts,
refundedProductQuantities
}: OrderRefundFormData
) =>
getProductsAmountValues(
order,
refundedFulfilledProductQuantities,
refundedProductQuantities,
refundShipmentCosts
);

View file

@ -0,0 +1,129 @@
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import Container from "@saleor/components/Container";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import { OrderErrorFragment } from "@saleor/fragments/types/OrderErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { renderCollection } from "@saleor/misc";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import OrderAmount from "../OrderRefundReturnAmount";
import { getReturnProductsAmountValues } from "../OrderRefundReturnAmount/utils";
import OrderRefundForm, { OrderRefundSubmitData } from "./form";
import ItemsCard from "./OrderReturnRefundItemsCard/ReturnItemsCard";
import {
getFulfilledFulfillemnts,
getParsedFulfiledLines,
getUnfulfilledLines
} from "./utils";
const messages = defineMessages({
appTitle: {
defaultMessage: "Order #{orderNumber}",
description: "page header with order number"
},
pageTitle: {
defaultMessage: "Order no. {orderNumber} - Replace/Return",
description: "page header"
}
});
export interface OrderReturnPageProps {
order: OrderDetails_order;
loading: boolean;
errors?: OrderErrorFragment[];
onBack: () => void;
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise;
}
const OrderRefundPage: React.FC<OrderReturnPageProps> = props => {
const { order, loading, errors = [], onBack, onSubmit } = props;
const intl = useIntl();
return (
<OrderRefundForm order={order} onSubmit={onSubmit}>
{({ data, handlers, change, submit }) => {
const { fulfiledItemsQuantities, unfulfiledItemsQuantities } = data;
const hasAnyItemsSelected =
fulfiledItemsQuantities.some(({ value }) => !!value) ||
unfulfiledItemsQuantities.some(({ value }) => !!value);
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(messages.appTitle, {
orderNumber: order?.number
})}
</AppHeader>
<PageHeader
title={intl.formatMessage(messages.pageTitle, {
orderNumber: order?.number
})}
/>
<Grid>
<div>
{!!data.unfulfiledItemsQuantities.length && (
<>
<ItemsCard
errors={errors}
order={order}
lines={getUnfulfilledLines(order)}
itemsQuantities={data.unfulfiledItemsQuantities}
itemsSelections={data.itemsToBeReplaced}
onChangeQuantity={handlers.changeUnfulfiledItemsQuantity}
onSetMaxQuantity={
handlers.handleSetMaximalUnfulfiledItemsQuantities
}
onChangeSelected={handlers.changeItemsToBeReplaced}
/>
<CardSpacer />
</>
)}
{renderCollection(
getFulfilledFulfillemnts(order),
({ id, lines }) => (
<React.Fragment key={id}>
<ItemsCard
errors={errors}
order={order}
fulfilmentId={id}
lines={getParsedFulfiledLines(lines)}
itemsQuantities={data.fulfiledItemsQuantities}
itemsSelections={data.itemsToBeReplaced}
onChangeQuantity={handlers.changeFulfiledItemsQuantity}
onSetMaxQuantity={handlers.handleSetMaximalFulfiledItemsQuantities(
id
)}
onChangeSelected={handlers.changeItemsToBeReplaced}
/>
<CardSpacer />
</React.Fragment>
)
)}
</div>
<div>
<OrderAmount
isReturn
amountData={getReturnProductsAmountValues(order, data)}
data={data}
order={order}
disableSubmitButton={!hasAnyItemsSelected}
disabled={loading}
errors={errors}
onChange={change}
onRefund={submit}
/>
</div>
</Grid>
</Container>
);
}}
</OrderRefundForm>
);
};
export default OrderRefundPage;

View file

@ -0,0 +1,135 @@
import { makeStyles, Typography } from "@material-ui/core";
import DefaultCardTitle from "@saleor/components/CardTitle";
import { StatusType } from "@saleor/components/StatusChip/types";
import StatusLabel from "@saleor/components/StatusLabel";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import camelCase from "lodash/camelCase";
import React from "react";
import { defineMessages } from "react-intl";
import { useIntl } from "react-intl";
const useStyles = makeStyles(
theme => ({
orderNumber: {
display: "inline",
marginLeft: theme.spacing(1)
}
}),
{ name: "CardTitle" }
);
const messages = defineMessages({
cancelled: {
defaultMessage: "Cancelled ({quantity})",
description: "cancelled fulfillment, section header"
},
fulfilled: {
defaultMessage: "Fulfilled ({quantity})",
description: "section header"
},
refunded: {
defaultMessage: "Refunded ({quantity})",
description: "refunded fulfillment, section header"
},
refundedAndReturned: {
defaultMessage: "Refunded and Returned ({quantity})",
description: "cancelled fulfillment, section header"
},
replaced: {
defaultMessage: "Replaced ({quantity})",
description: "refunded fulfillment, section header"
},
returned: {
defaultMessage: "Returned ({quantity})",
description: "refunded fulfillment, section header"
},
unfulfilled: {
defaultMessage: "Unfulfilled",
description: "section header"
}
});
type CardTitleStatus = FulfillmentStatus | "unfulfilled";
type CardTitleLines = Array<{
quantity: number;
}>;
interface CardTitleProps {
lines?: CardTitleLines;
fulfillmentOrder?: number;
status: CardTitleStatus;
toolbar?: React.ReactNode;
orderNumber?: string;
withStatus?: boolean;
}
const selectStatus = (status: CardTitleStatus) => {
switch (status) {
case FulfillmentStatus.FULFILLED:
return StatusType.SUCCESS;
case FulfillmentStatus.REFUNDED:
return StatusType.NEUTRAL;
case FulfillmentStatus.RETURNED:
return StatusType.NEUTRAL;
case FulfillmentStatus.REPLACED:
return StatusType.NEUTRAL;
case FulfillmentStatus.REFUNDED_AND_RETURNED:
return StatusType.NEUTRAL;
case FulfillmentStatus.CANCELED:
return StatusType.ERROR;
default:
return StatusType.ALERT;
}
};
const CardTitle: React.FC<CardTitleProps> = ({
lines = [],
fulfillmentOrder,
status,
orderNumber = "",
withStatus = false,
toolbar
}) => {
const intl = useIntl();
const classes = useStyles({});
const fulfillmentName =
orderNumber && fulfillmentOrder
? `#${orderNumber}-${fulfillmentOrder}`
: "";
const messageForStatus = messages[camelCase(status)] || messages.unfulfilled;
const totalQuantity = lines.reduce(
(resultQuantity, { quantity }) => resultQuantity + quantity,
0
);
const title = (
<>
{intl.formatMessage(messageForStatus, {
fulfillmentName,
quantity: totalQuantity
})}
<Typography className={classes.orderNumber} variant="body1">
{fulfillmentName}
</Typography>
</>
);
return (
<DefaultCardTitle
toolbar={toolbar}
title={
withStatus ? (
<StatusLabel label={title} status={selectStatus(status)} />
) : (
title
)
}
/>
);
};
export default CardTitle;

View file

@ -0,0 +1,39 @@
import { Button, makeStyles } from "@material-ui/core";
import React from "react";
import { FormattedMessage } from "react-intl";
const useStyles = makeStyles(
theme => ({
button: {
letterSpacing: 2,
marginBottom: theme.spacing(1),
marginTop: theme.spacing(3),
padding: 0
}
}),
{ name: "MaximalButton" }
);
interface MaximalButtonProps {
onClick: () => void;
}
const MaximalButton: React.FC<MaximalButtonProps> = ({ onClick }) => {
const classes = useStyles({});
return (
<Button
className={classes.button}
color="primary"
onClick={onClick}
data-test="setMaximalQuantityUnfulfilledButton"
>
<FormattedMessage
defaultMessage="Set maximal quantities"
description="button"
/>
</Button>
);
};
export default MaximalButton;

View file

@ -0,0 +1,100 @@
import Popper from "@material-ui/core/Popper";
import makeStyles from "@material-ui/core/styles/makeStyles";
import TableCell from "@material-ui/core/TableCell";
import Typography from "@material-ui/core/Typography";
import ErrorExclamationCircleIcon from "@saleor/icons/ErrorExclamationCircle";
import React, { useState } from "react";
import { defineMessages } from "react-intl";
import { useIntl } from "react-intl";
const useStyles = makeStyles(
theme => ({
container: {
position: "relative"
},
errorBox: {
backgroundColor: theme.palette.error.main,
borderRadius: 8,
marginRight: theme.spacing(3),
padding: theme.spacing(2, 3),
width: 280,
zIndex: 1000
},
errorText: {
color: "white",
fontSize: 14
},
errorTextHighlighted: {
color: theme.palette.error.main,
fontSize: 12,
marginRight: theme.spacing(1)
},
titleContainer: {
alignItems: "center",
display: "flex",
flexDirection: "row",
justifyContent: "flex-end"
}
}),
{ name: "ProductErrorCell" }
);
const messages = defineMessages({
description: {
defaultMessage:
"This product is no longer in database so it cant be replaced, nor returned",
description: "product no longer exists error description"
},
title: {
defaultMessage: "Product no longer exists",
description: "product no longer exists error title"
}
});
interface ProductErrorCellProps {
hasVariant: boolean;
}
const ProductErrorCell: React.FC<ProductErrorCellProps> = ({ hasVariant }) => {
const classes = useStyles({});
const intl = useIntl();
const popperAnchorRef = React.useRef<HTMLButtonElement | null>(null);
const [showErrorBox, setShowErrorBox] = useState<boolean>(false);
if (hasVariant) {
return <TableCell />;
}
return (
<TableCell
align="right"
className={classes.container}
ref={popperAnchorRef}
>
<div
className={classes.titleContainer}
onMouseEnter={() => setShowErrorBox(true)}
onMouseLeave={() => setShowErrorBox(false)}
>
<Typography className={classes.errorTextHighlighted}>
{intl.formatMessage(messages.title)}
</Typography>
<ErrorExclamationCircleIcon />
</div>
<Popper
placement="bottom-end"
open={showErrorBox}
anchorEl={popperAnchorRef.current}
>
<div className={classes.errorBox}>
<Typography className={classes.errorText}>
{intl.formatMessage(messages.description)}
</Typography>
</div>
</Popper>
</TableCell>
);
};
export default ProductErrorCell;

View file

@ -0,0 +1,262 @@
import {
Card,
CardContent,
Checkbox,
makeStyles,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField
} from "@material-ui/core";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { OrderErrorFragment } from "@saleor/fragments/types/OrderErrorFragment";
import { FormsetChange } from "@saleor/hooks/useFormset";
import { renderCollection } from "@saleor/misc";
import {
OrderDetails_order,
OrderDetails_order_lines
} from "@saleor/orders/types/OrderDetails";
import React, { CSSProperties } from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import { FormsetQuantityData, FormsetReplacementData } from "../form";
import { getById } from "../utils";
import CardTitle from "./CardTitle";
import MaximalButton from "./MaximalButton";
import ProductErrorCell from "./ProductErrorCell";
const useStyles = makeStyles(
theme => {
const inputPadding: CSSProperties = {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2)
};
return {
cartContent: {
paddingBottom: 0,
paddingTop: 0
},
notice: {
marginBottom: theme.spacing(1),
marginTop: theme.spacing(2)
},
quantityInnerInput: {
...inputPadding
},
quantityInnerInputNoRemaining: {
paddingRight: 0
},
remainingQuantity: {
...inputPadding,
color: theme.palette.text.secondary,
whiteSpace: "nowrap"
},
setMaximalQuantityButton: {
marginBottom: theme.spacing(1),
marginTop: theme.spacing(2),
padding: 0
}
};
},
{ name: "ItemsCard" }
);
const messages = defineMessages({
improperValue: {
defaultMessage: "Improper value",
description: "error message"
},
titleFulfilled: {
defaultMessage: "Fulfillment - #{fulfilmentId}",
description: "section header"
},
titleUnfulfilled: {
defaultMessage: "Unfulfilled Items",
description: "section header"
}
});
interface OrderReturnRefundLinesCardProps {
onChangeQuantity: FormsetChange<number>;
fulfilmentId?: string;
canReplace?: boolean;
errors: OrderErrorFragment[];
lines: OrderDetails_order_lines[];
order: OrderDetails_order;
itemsSelections: FormsetReplacementData;
itemsQuantities: FormsetQuantityData;
onChangeSelected: FormsetChange<boolean>;
onSetMaxQuantity();
}
const ItemsCard: React.FC<OrderReturnRefundLinesCardProps> = ({
lines,
onSetMaxQuantity,
onChangeQuantity,
onChangeSelected,
itemsSelections,
itemsQuantities,
fulfilmentId,
order
}) => {
const classes = useStyles({});
const intl = useIntl();
const handleChangeQuantity = (id: string) => (
event: React.ChangeEvent<HTMLInputElement>
) => onChangeQuantity(id, parseInt(event.target.value, 10));
const fulfillment = order?.fulfillments.find(getById(fulfilmentId));
return (
<Card>
<CardTitle
orderNumber={order?.number}
lines={lines}
fulfillmentOrder={fulfillment?.fulfillmentOrder}
status={fulfillment?.status}
/>
<CardContent className={classes.cartContent}>
<MaximalButton onClick={onSetMaxQuantity} />
</CardContent>
<Table>
<TableHead>
<TableRow>
<TableCell>
<FormattedMessage
defaultMessage="Product"
description="table column header"
/>
</TableCell>
<TableCell />
<TableCell align="right">
<FormattedMessage
defaultMessage="Price"
description="table column header"
/>
</TableCell>
<TableCell align="right">
<FormattedMessage
defaultMessage="Return"
description="table column header"
/>
</TableCell>
<TableCell align="center">
<FormattedMessage
defaultMessage="Replace"
description="table column header"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
lines,
line => {
const {
quantity,
quantityFulfilled,
id,
thumbnail,
unitPrice,
productName,
variant
} = line;
const isValueError = false;
const isRefunded = itemsQuantities.find(getById(id)).data
.isRefunded;
const isReplacable = !!variant && !isRefunded;
const isReturnable = !!variant;
const lineQuantity = fulfilmentId
? quantity
: quantity - quantityFulfilled;
const isSelected = itemsSelections.find(getById(id))?.value;
const currentQuantity = itemsQuantities.find(getById(id))?.value;
const anyLineWithoutVariant = lines.some(
({ variant }) => !variant
);
const productNameCellWidth = anyLineWithoutVariant
? "30%"
: "50%";
return (
<TableRow key={id}>
<TableCellAvatar
thumbnail={thumbnail?.url}
style={{ width: productNameCellWidth }}
>
{productName || <Skeleton />}
</TableCellAvatar>
<ProductErrorCell hasVariant={isReturnable} />
<TableCell align="right">
<Money
money={{
amount: unitPrice.gross.amount,
currency: unitPrice.gross.currency
}}
/>
</TableCell>
<TableCell align="right">
{isReturnable && (
<TextField
type="number"
inputProps={{
className: classes.quantityInnerInput,
"data-test": "quantityInput",
"data-test-id": id,
max: lineQuantity.toString(),
min: 0,
style: { textAlign: "right" }
}}
fullWidth
value={currentQuantity}
onChange={handleChangeQuantity(id)}
InputProps={{
endAdornment: lineQuantity && (
<div className={classes.remainingQuantity}>
/ {lineQuantity}
</div>
)
}}
error={isValueError}
helperText={
isValueError &&
intl.formatMessage(messages.improperValue)
}
/>
)}
</TableCell>
<TableCell align="center">
{isReplacable && (
<Checkbox
checked={isSelected}
onChange={() => onChangeSelected(id, !isSelected)}
/>
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={4}>
<Skeleton />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
);
};
export default ItemsCard;

View file

@ -0,0 +1,2 @@
export * from "./ReturnItemsCard";
export { default } from "./ReturnItemsCard";

View file

@ -0,0 +1,233 @@
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import React, { useState } from "react";
import { OrderRefundAmountCalculationMode } from "../OrderRefundPage/form";
import {
getById,
getLineItem,
getOrderUnfulfilledLines,
getParsedLineData,
getParsedLineDataForFulfillmentStatus
} from "./utils";
export interface LineItemOptions<T> {
initialValue: T;
isFulfillment?: boolean;
isRefunded?: boolean;
}
export interface LineItemData {
isFulfillment: boolean;
isRefunded: boolean;
}
export type FormsetQuantityData = FormsetData<LineItemData, number>;
export type FormsetReplacementData = FormsetData<LineItemData, boolean>;
export interface OrderReturnData {
amount: number;
refundShipmentCosts: boolean;
amountCalculationMode: OrderRefundAmountCalculationMode;
}
export interface OrderReturnHandlers {
changeFulfiledItemsQuantity: FormsetChange<number>;
changeUnfulfiledItemsQuantity: FormsetChange<number>;
changeItemsToBeReplaced: FormsetChange<boolean>;
handleSetMaximalFulfiledItemsQuantities;
handleSetMaximalUnfulfiledItemsQuantities;
}
export interface OrderReturnFormData extends OrderReturnData {
itemsToBeReplaced: FormsetReplacementData;
fulfiledItemsQuantities: FormsetQuantityData;
unfulfiledItemsQuantities: FormsetQuantityData;
}
export type OrderRefundSubmitData = OrderReturnFormData;
export interface UseOrderRefundFormResult {
change: FormChange;
hasChanged: boolean;
data: OrderReturnFormData;
handlers: OrderReturnHandlers;
submit: () => Promise<boolean>;
}
interface OrderReturnProps {
children: (props: UseOrderRefundFormResult) => React.ReactNode;
order: OrderDetails_order;
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise;
}
const getOrderRefundPageFormData = (): OrderReturnData => ({
amount: undefined,
amountCalculationMode: OrderRefundAmountCalculationMode.AUTOMATIC,
refundShipmentCosts: false
});
function useOrderReturnForm(
order: OrderDetails_order,
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise
): UseOrderRefundFormResult {
const form = useForm(getOrderRefundPageFormData());
const [hasChanged, setHasChanged] = useState(false);
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
};
const unfulfiledItemsQuantites = useFormset<LineItemData, number>(
getOrderUnfulfilledLines(order).map(getParsedLineData({ initialValue: 0 }))
);
const getItemsFulfilled = () => {
const commonOptions = {
initialValue: 0,
isFulfillment: true
};
const refundedFulfilmentsItems = getParsedLineDataForFulfillmentStatus(
order,
FulfillmentStatus.REFUNDED,
{ ...commonOptions, isRefunded: true }
);
const fulfilledFulfillmentsItems = getParsedLineDataForFulfillmentStatus(
order,
FulfillmentStatus.FULFILLED,
commonOptions
);
return refundedFulfilmentsItems.concat(fulfilledFulfillmentsItems);
};
const fulfiledItemsQuatities = useFormset<LineItemData, number>(
getItemsFulfilled()
);
const getItemsToBeReplaced = () => {
if (!order) {
return [];
}
const orderLinesItems = getOrderUnfulfilledLines(order).map(
getParsedLineData({ initialValue: false })
);
const refundedFulfilmentsItems = getParsedLineDataForFulfillmentStatus(
order,
FulfillmentStatus.REFUNDED,
{ initialValue: false, isFulfillment: true }
);
const fulfilledFulfillmentsItems = getParsedLineDataForFulfillmentStatus(
order,
FulfillmentStatus.FULFILLED,
{ initialValue: false, isFulfillment: true }
);
return [
...orderLinesItems,
...refundedFulfilmentsItems,
...fulfilledFulfillmentsItems
];
};
const itemsToBeReplaced = useFormset<LineItemData, boolean>(
getItemsToBeReplaced()
);
const handleSetMaximalUnfulfiledItemsQuantities = () => {
const newQuantities: FormsetQuantityData = unfulfiledItemsQuantites.data.map(
({ id }) => {
const line = order.lines.find(getById(id));
const initialValue = line.quantity - line.quantityFulfilled;
return getLineItem(line, { initialValue });
}
);
triggerChange();
unfulfiledItemsQuantites.set(newQuantities);
};
const handleSetMaximalFulfiledItemsQuantities = (
fulfillmentId: string
) => () => {
const { lines } = order.fulfillments.find(getById(fulfillmentId));
const newQuantities: FormsetQuantityData = fulfiledItemsQuatities.data.map(
item => {
const line = lines.find(getById(item.id));
if (!line) {
return item;
}
return getLineItem(line, {
initialValue: line.quantity,
isRefunded: item.data.isRefunded
});
}
);
triggerChange();
fulfiledItemsQuatities.set(newQuantities);
};
const data: OrderReturnFormData = {
fulfiledItemsQuantities: fulfiledItemsQuatities.data,
itemsToBeReplaced: itemsToBeReplaced.data,
unfulfiledItemsQuantities: unfulfiledItemsQuantites.data,
...form.data
};
const submit = () => handleFormSubmit(data, onSubmit, setHasChanged);
const triggerChange = () => setHasChanged(true);
function handleHandlerChange<T>(callback: (id: string, value: T) => void) {
return (id: string, value: T) => {
triggerChange();
callback(id, value);
};
}
return {
change: handleChange,
data,
handlers: {
changeFulfiledItemsQuantity: handleHandlerChange(
fulfiledItemsQuatities.change
),
changeItemsToBeReplaced: handleHandlerChange(itemsToBeReplaced.change),
changeUnfulfiledItemsQuantity: handleHandlerChange(
unfulfiledItemsQuantites.change
),
handleSetMaximalFulfiledItemsQuantities,
handleSetMaximalUnfulfiledItemsQuantities
},
hasChanged,
submit
};
}
const OrderReturnForm: React.FC<OrderReturnProps> = ({
children,
order,
onSubmit
}) => {
const props = useOrderReturnForm(order, onSubmit);
return <form>{children(props)}</form>;
};
export default OrderReturnForm;

View file

@ -0,0 +1,2 @@
export * from "./OrderReturnPage";
export { default } from "./OrderReturnPage";

View file

@ -0,0 +1,93 @@
import { OrderDetailsFragment_fulfillments_lines } from "@saleor/fragments/types/OrderDetailsFragment";
import {
OrderDetails_order,
OrderDetails_order_fulfillments
} from "@saleor/orders/types/OrderDetails";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import { LineItemOptions } from "./form";
const fulfiledStatuses = [
FulfillmentStatus.FULFILLED,
FulfillmentStatus.REFUNDED
];
export const getOrderUnfulfilledLines = (order: OrderDetails_order) =>
order?.lines.filter(line => line.quantityFulfilled !== line.quantity) || [];
export const getFulfilledFulfillment = fulfillment =>
fulfiledStatuses.includes(fulfillment.status);
export const getFulfilledFulfillemnts = (order?: OrderDetails_order) =>
order?.fulfillments.filter(getFulfilledFulfillment) || [];
export const getUnfulfilledLines = (order?: OrderDetails_order) =>
order?.lines.filter(line => line.quantity !== line.quantityFulfilled) || [];
export const getAllOrderFulfilledLines = (order?: OrderDetails_order) =>
getFulfilledFulfillemnts(order).reduce(
(result, { lines }) => [...result, ...getParsedFulfiledLines(lines)],
[]
);
export function getLineItem<T>(
{ id }: { id: string },
{
initialValue,
isFulfillment = false,
isRefunded = false
}: LineItemOptions<T>
) {
return {
data: { isFulfillment, isRefunded },
id,
label: null,
value: initialValue
};
}
export function getParsedLineData<T>({
initialValue,
isFulfillment = false,
isRefunded = false
}: LineItemOptions<T>) {
return (item: { id: string }) =>
getLineItem(item, { initialValue, isFulfillment, isRefunded });
}
export function getParsedLineDataForFulfillmentStatus<T>(
order: OrderDetails_order,
fulfillmentStatus: FulfillmentStatus,
lineItemOptions: LineItemOptions<T>
) {
return getParsedLinesOfFulfillments(
getFulfillmentsWithStatus(order, fulfillmentStatus)
).map(getParsedLineData(lineItemOptions));
}
export const getFulfillmentsWithStatus = (
order: OrderDetails_order,
fulfillmentStatus: FulfillmentStatus
) =>
order?.fulfillments.filter(({ status }) => status === fulfillmentStatus) ||
[];
export const getParsedLinesOfFulfillments = (
fullfillments: OrderDetails_order_fulfillments[]
) =>
fullfillments.reduce(
(result, { lines }) => [...result, ...getParsedFulfiledLines(lines)],
[]
);
export const getParsedFulfiledLines = (
lines: OrderDetailsFragment_fulfillments_lines[]
) =>
lines.map(({ id, quantity, orderLine }) => ({
...orderLine,
id,
quantity
}));
export const getById = (idToCompare: string) => (obj: { id: string }) =>
obj.id === idToCompare;

View file

@ -1,192 +0,0 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import CardTitle from "@saleor/components/CardTitle";
import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { maybe } from "../../../misc";
import { OrderDetails_order_lines } from "../../types/OrderDetails";
const useStyles = makeStyles(
{
clickableRow: {
cursor: "pointer"
},
colName: {
paddingLeft: 0,
width: "auto"
},
colNameLabel: {
marginLeft: AVATAR_MARGIN
},
colPrice: {
textAlign: "right",
width: 120
},
colQuantity: {
textAlign: "center",
width: 120
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 120
},
colTotal: {
textAlign: "right",
width: 120
},
statusBar: {
paddingTop: 0
},
table: {
tableLayout: "fixed"
}
},
{ name: "OrderUnfulfilledItems" }
);
interface OrderUnfulfilledItemsProps {
canFulfill: boolean;
lines: OrderDetails_order_lines[];
onFulfill: () => void;
}
const OrderUnfulfilledItems: React.FC<OrderUnfulfilledItemsProps> = props => {
const { canFulfill, lines, onFulfill } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<CardTitle
title={
<StatusLabel
label={intl.formatMessage(
{
defaultMessage: "Unfulfilled ({quantity})",
description: "section header"
},
{
quantity: lines
.map(line => line.quantity - line.quantityFulfilled)
.reduce((prev, curr) => prev + curr, 0)
}
)}
status="error"
/>
}
/>
<ResponsiveTable className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<span className={classes.colNameLabel}>
<FormattedMessage
defaultMessage="Product"
description="product name"
/>
</span>
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage
defaultMessage="SKU"
description="ordered product sku"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
defaultMessage="Quantity"
description="ordered products"
/>
</TableCell>
<TableCell className={classes.colPrice}>
<FormattedMessage
defaultMessage="Price"
description="product unit price"
/>
</TableCell>
<TableCell className={classes.colTotal}>
<FormattedMessage
defaultMessage="Total"
description="order line total price"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{lines.map(line => (
<TableRow
className={!!line ? classes.clickableRow : undefined}
hover={!!line}
key={maybe(() => line.id)}
>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => line.thumbnail.url)}
>
{maybe(() => line.productName) || <Skeleton />}
</TableCellAvatar>
<TableCell className={classes.colSku}>
{line?.productSku || <Skeleton />}
</TableCell>
<TableCell className={classes.colQuantity}>
{maybe(() => line.quantity - line.quantityFulfilled) || (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPrice}>
{maybe(() => line.unitPrice.gross) ? (
<Money money={line.unitPrice.gross} />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTotal}>
{maybe(
() =>
(line.quantity - line.quantityFulfilled) *
line.unitPrice.gross.amount
) ? (
<Money
money={{
amount:
(line.quantity - line.quantityFulfilled) *
line.unitPrice.gross.amount,
currency: line.unitPrice.gross.currency
}}
/>
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</ResponsiveTable>
{canFulfill && (
<CardActions>
<Button variant="text" color="primary" onClick={onFulfill}>
<FormattedMessage defaultMessage="Fulfill" description="button" />
</Button>
</CardActions>
)}
</Card>
);
};
OrderUnfulfilledItems.displayName = "OrderUnfulfilledItems";
export default OrderUnfulfilledItems;

View file

@ -1,2 +0,0 @@
export { default } from "./OrderUnfulfilledItems";
export * from "./OrderUnfulfilledItems";

View file

@ -0,0 +1,64 @@
import { makeStyles, TableBody } from "@material-ui/core";
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import CardSpacer from "@saleor/components/CardSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { renderCollection } from "@saleor/misc";
import React from "react";
import { FormattedMessage } from "react-intl";
import { OrderDetails_order_lines } from "../../types/OrderDetails";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import CardTitle from "../OrderReturnPage/OrderReturnRefundItemsCard/CardTitle";
const useStyles = makeStyles(
() => ({
table: {
tableLayout: "fixed"
}
}),
{ name: "OrderUnfulfilledItems" }
);
interface OrderUnfulfilledProductsCardProps {
canFulfill: boolean;
lines: OrderDetails_order_lines[];
onFulfill: () => void;
}
const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps> = props => {
const { canFulfill, lines, onFulfill } = props;
const classes = useStyles({});
if (!lines.length) {
return null;
}
return (
<>
<Card>
<CardTitle withStatus status="unfulfilled" />
<ResponsiveTable className={classes.table}>
<TableHeader />
<TableBody>
{renderCollection(lines, line => (
<TableLine isOrderLine line={line} />
))}
</TableBody>
</ResponsiveTable>
{canFulfill && (
<CardActions>
<Button variant="text" color="primary" onClick={onFulfill}>
<FormattedMessage defaultMessage="Fulfill" description="button" />
</Button>
</CardActions>
)}
</Card>
<CardSpacer />
</>
);
};
export default OrderUnfulfilledProductsCard;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderUnfulfilledProductsCard";
export * from "./OrderUnfulfilledProductsCard";

View file

@ -834,13 +834,16 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: null,
quantity: 1,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "123",
type: OrderEventsEnum.FULFILLMENT_FULFILLED_ITEMS,
user: {
__typename: "User",
email: "admin@example.com",
id: "QWRkcmVzczoxNQ=="
firstName: "John",
id: "QWRkcmVzczoxNQ==",
lastName: "Doe"
}
},
{
@ -875,13 +878,16 @@ export const order = (placeholder: string): OrderDetails_order => ({
],
message: null,
quantity: 1,
relatedOrder: null,
shippingCostsIncluded: true,
transactionReference: "123",
type: OrderEventsEnum.FULFILLMENT_REFUNDED,
user: {
__typename: "User",
email: "admin@example.com",
id: "QWRkcmVzczoxNQ=="
firstName: "Jane",
id: "QWRkcmVzczoxNQ==",
lastName: "Doe"
}
},
{
@ -895,6 +901,7 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: "This is note",
quantity: null,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "124",
type: OrderEventsEnum.NOTE_ADDED,
@ -911,6 +918,7 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: "This is note",
quantity: null,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "125",
type: OrderEventsEnum.NOTE_ADDED,
@ -927,6 +935,7 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: "Note from external service",
quantity: null,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "126",
type: OrderEventsEnum.EXTERNAL_SERVICE_NOTIFICATION,
@ -943,6 +952,7 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: null,
quantity: null,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "127",
type: OrderEventsEnum.EMAIL_SENT,
@ -959,6 +969,7 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: null,
quantity: null,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "128",
type: OrderEventsEnum.EMAIL_SENT,
@ -975,6 +986,7 @@ export const order = (placeholder: string): OrderDetails_order => ({
lines: [],
message: null,
quantity: null,
relatedOrder: null,
shippingCostsIncluded: false,
transactionReference: "129",
type: OrderEventsEnum.PAYMENT_AUTHORIZED,

View file

@ -16,6 +16,7 @@ import {
OrderListUrlSortField,
orderPath,
orderRefundPath,
orderReturnPath,
orderSettingsPath,
OrderUrlQueryParams
} from "./urls";
@ -24,6 +25,7 @@ import OrderDraftListComponent from "./views/OrderDraftList";
import OrderFulfillComponent from "./views/OrderFulfill";
import OrderListComponent from "./views/OrderList";
import OrderRefundComponent from "./views/OrderRefund";
import OrderReturnComponent from "./views/OrderReturn";
import OrderSettings from "./views/OrderSettings";
const OrderList: React.FC<RouteComponentProps<any>> = ({ location }) => {
@ -71,6 +73,10 @@ const OrderRefund: React.FC<RouteComponentProps<any>> = ({ match }) => (
<OrderRefundComponent orderId={decodeURIComponent(match.params.id)} />
);
const OrderReturn: React.FC<RouteComponentProps<any>> = ({ match }) => (
<OrderReturnComponent orderId={decodeURIComponent(match.params.id)} />
);
const Component = () => {
const intl = useIntl();
@ -82,6 +88,7 @@ const Component = () => {
<Route exact path={orderDraftListPath} component={OrderDraftList} />
<Route exact path={orderListPath} component={OrderList} />
<Route path={orderFulfillPath(":id")} component={OrderFulfill} />
<Route path={orderReturnPath(":id")} component={OrderReturn} />
<Route path={orderRefundPath(":id")} component={OrderRefund} />
<Route path={orderPath(":id")} component={OrderDetails} />
</Switch>

View file

@ -14,6 +14,10 @@ import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag";
import { TypedMutation } from "../mutations";
import {
FulfillmentReturnProducts,
FulfillmentReturnProductsVariables
} from "./types/FulfillmentReturnProducts";
import { FulfillOrder, FulfillOrderVariables } from "./types/FulfillOrder";
import {
InvoiceEmailSend,
@ -172,6 +176,32 @@ const orderDraftFinalizeMutation = gql`
}
}
`;
const orderReturnCreateMutation = gql`
${orderErrorFragment}
mutation FulfillmentReturnProducts(
$id: ID!
$input: OrderReturnProductsInput!
) {
orderFulfillmentReturnProducts(input: $input, order: $id) {
errors: orderErrors {
...OrderErrorFragment
}
order {
id
}
replaceOrder {
id
}
}
}
`;
export const useOrderReturnCreateMutation = makeMutation<
FulfillmentReturnProducts,
FulfillmentReturnProductsVariables
>(orderReturnCreateMutation);
export const TypedOrderDraftFinalizeMutation = TypedMutation<
OrderDraftFinalize,
OrderDraftFinalizeVariables
@ -191,6 +221,7 @@ const orderRefundMutation = gql`
}
}
`;
export const useOrderRefundMutation = makeMutation<
OrderRefund,
OrderRefundVariables

View file

@ -157,6 +157,10 @@ export const TypedOrderDetailsQuery = TypedQuery<
OrderDetailsVariables
>(orderDetailsQuery);
export const useOrderQuery = makeQuery<OrderDetails, OrderDetailsVariables>(
orderDetailsQuery
);
export const searchOrderVariant = gql`
query SearchOrderVariant($first: Int!, $query: String!, $after: String) {
search: products(first: $first, after: $after, filter: { search: $query }) {

View file

@ -50,10 +50,18 @@ export interface FulfillOrder_orderFulfill_order_billingAddress {
streetAddress2: string;
}
export interface FulfillOrder_orderFulfill_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface FulfillOrder_orderFulfill_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface FulfillOrder_orderFulfill_order_events_lines_orderLine {
@ -78,6 +86,7 @@ export interface FulfillOrder_orderFulfill_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: FulfillOrder_orderFulfill_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -0,0 +1,41 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { OrderReturnProductsInput, OrderErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: FulfillmentReturnProducts
// ====================================================
export interface FulfillmentReturnProducts_orderFulfillmentReturnProducts_errors {
__typename: "OrderError";
code: OrderErrorCode;
field: string | null;
}
export interface FulfillmentReturnProducts_orderFulfillmentReturnProducts_order {
__typename: "Order";
id: string;
}
export interface FulfillmentReturnProducts_orderFulfillmentReturnProducts_replaceOrder {
__typename: "Order";
id: string;
}
export interface FulfillmentReturnProducts_orderFulfillmentReturnProducts {
__typename: "FulfillmentReturnProducts";
errors: FulfillmentReturnProducts_orderFulfillmentReturnProducts_errors[];
order: FulfillmentReturnProducts_orderFulfillmentReturnProducts_order | null;
replaceOrder: FulfillmentReturnProducts_orderFulfillmentReturnProducts_replaceOrder | null;
}
export interface FulfillmentReturnProducts {
orderFulfillmentReturnProducts: FulfillmentReturnProducts_orderFulfillmentReturnProducts | null;
}
export interface FulfillmentReturnProductsVariables {
id: string;
input: OrderReturnProductsInput;
}

View file

@ -14,10 +14,18 @@ export interface OrderAddNote_orderAddNote_errors {
field: string | null;
}
export interface OrderAddNote_orderAddNote_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderAddNote_orderAddNote_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderAddNote_orderAddNote_order_events_lines_orderLine {
@ -42,6 +50,7 @@ export interface OrderAddNote_orderAddNote_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderAddNote_orderAddNote_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderCancel_orderCancel_order_billingAddress {
streetAddress2: string;
}
export interface OrderCancel_orderCancel_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderCancel_orderCancel_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderCancel_orderCancel_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderCancel_orderCancel_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderCancel_orderCancel_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderCapture_orderCapture_order_billingAddress {
streetAddress2: string;
}
export interface OrderCapture_orderCapture_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderCapture_orderCapture_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderCapture_orderCapture_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderCapture_orderCapture_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderCapture_orderCapture_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderConfirm_orderConfirm_order_billingAddress {
streetAddress2: string;
}
export interface OrderConfirm_orderConfirm_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderConfirm_orderConfirm_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderConfirm_orderConfirm_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderConfirm_orderConfirm_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderConfirm_orderConfirm_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -42,10 +42,18 @@ export interface OrderDetails_order_billingAddress {
streetAddress2: string;
}
export interface OrderDetails_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderDetails_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderDetails_order_events_lines_orderLine {
@ -70,6 +78,7 @@ export interface OrderDetails_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderDetails_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderDraftCancel_draftOrderDelete_order_billingAddress {
streetAddress2: string;
}
export interface OrderDraftCancel_draftOrderDelete_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderDraftCancel_draftOrderDelete_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderDraftCancel_draftOrderDelete_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderDraftCancel_draftOrderDelete_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderDraftCancel_draftOrderDelete_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderDraftFinalize_draftOrderComplete_order_billingAddress {
streetAddress2: string;
}
export interface OrderDraftFinalize_draftOrderComplete_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderDraftFinalize_draftOrderComplete_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderDraftFinalize_draftOrderComplete_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderDraftFinalize_draftOrderComplete_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderDraftFinalize_draftOrderComplete_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderDraftUpdate_draftOrderUpdate_order_billingAddress {
streetAddress2: string;
}
export interface OrderDraftUpdate_draftOrderUpdate_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderDraftUpdate_draftOrderUpdate_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderDraftUpdate_draftOrderUpdate_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderDraftUpdate_draftOrderUpdate_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderDraftUpdate_draftOrderUpdate_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_billingAddr
streetAddress2: string;
}
export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderFulfillmentCancel_orderFulfillmentCancel_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -113,10 +113,18 @@ export interface OrderFulfillmentRefundProducts_orderFulfillmentRefundProducts_o
streetAddress2: string;
}
export interface OrderFulfillmentRefundProducts_orderFulfillmentRefundProducts_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderFulfillmentRefundProducts_orderFulfillmentRefundProducts_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderFulfillmentRefundProducts_orderFulfillmentRefundProducts_order_events_lines_orderLine {
@ -141,6 +149,7 @@ export interface OrderFulfillmentRefundProducts_orderFulfillmentRefundProducts_o
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderFulfillmentRefundProducts_orderFulfillmentRefundProducts_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_o
streetAddress2: string;
}
export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_o
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderLineDelete_draftOrderLineDelete_order_billingAddress {
streetAddress2: string;
}
export interface OrderLineDelete_draftOrderLineDelete_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderLineDelete_draftOrderLineDelete_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderLineDelete_draftOrderLineDelete_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderLineDelete_draftOrderLineDelete_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderLineDelete_draftOrderLineDelete_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderLineUpdate_draftOrderLineUpdate_order_billingAddress {
streetAddress2: string;
}
export interface OrderLineUpdate_draftOrderLineUpdate_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderLineUpdate_draftOrderLineUpdate_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderLineUpdate_draftOrderLineUpdate_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderLineUpdate_draftOrderLineUpdate_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderLineUpdate_draftOrderLineUpdate_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderLinesAdd_draftOrderLinesCreate_order_billingAddress {
streetAddress2: string;
}
export interface OrderLinesAdd_draftOrderLinesCreate_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderLinesAdd_draftOrderLinesCreate_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderLinesAdd_draftOrderLinesCreate_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderLinesAdd_draftOrderLinesCreate_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderLinesAdd_draftOrderLinesCreate_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderMarkAsPaid_orderMarkAsPaid_order_billingAddress {
streetAddress2: string;
}
export interface OrderMarkAsPaid_orderMarkAsPaid_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderMarkAsPaid_orderMarkAsPaid_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderMarkAsPaid_orderMarkAsPaid_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderMarkAsPaid_orderMarkAsPaid_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderMarkAsPaid_orderMarkAsPaid_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderRefund_orderRefund_order_billingAddress {
streetAddress2: string;
}
export interface OrderRefund_orderRefund_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderRefund_orderRefund_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderRefund_orderRefund_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderRefund_orderRefund_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderRefund_orderRefund_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderUpdate_orderUpdate_order_billingAddress {
streetAddress2: string;
}
export interface OrderUpdate_orderUpdate_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderUpdate_orderUpdate_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderUpdate_orderUpdate_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderUpdate_orderUpdate_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderUpdate_orderUpdate_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -48,10 +48,18 @@ export interface OrderVoid_orderVoid_order_billingAddress {
streetAddress2: string;
}
export interface OrderVoid_orderVoid_order_events_relatedOrder {
__typename: "Order";
id: string;
number: string | null;
}
export interface OrderVoid_orderVoid_order_events_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
}
export interface OrderVoid_orderVoid_order_events_lines_orderLine {
@ -76,6 +84,7 @@ export interface OrderVoid_orderVoid_order_events {
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
relatedOrder: OrderVoid_orderVoid_order_events_relatedOrder | null;
message: string | null;
quantity: number | null;
transactionReference: string | null;

View file

@ -99,6 +99,7 @@ export const orderDraftListUrl = (
};
export const orderPath = (id: string) => urlJoin(orderSectionUrl, id);
export type OrderUrlDialog =
| "add-order-line"
| "cancel"
@ -112,17 +113,26 @@ export type OrderUrlDialog =
| "mark-paid"
| "void"
| "invoice-send";
export type OrderUrlQueryParams = Dialog<OrderUrlDialog> & SingleAction;
export const orderUrl = (id: string, params?: OrderUrlQueryParams) =>
orderPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const orderFulfillPath = (id: string) =>
urlJoin(orderPath(id), "fulfill");
export const orderReturnPath = (id: string) => urlJoin(orderPath(id), "return");
export const orderFulfillUrl = (id: string) =>
orderFulfillPath(encodeURIComponent(id));
export const orderSettingsPath = urlJoin(orderSectionUrl, "settings");
export const orderRefundPath = (id: string) => urlJoin(orderPath(id), "refund");
export const orderRefundUrl = (id: string) =>
orderRefundPath(encodeURIComponent(id));
export const orderReturnUrl = (id: string) =>
orderReturnPath(encodeURIComponent(id));

View file

@ -1,8 +1,17 @@
/* eslint-disable sort-keys */
import { FormsetData } from "@saleor/hooks/useFormset";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import {
FulfillmentStatus,
OrderStatus,
PaymentChargeStatusEnum
} from "@saleor/types/globalTypes";
import { OrderDetails_order_fulfillments_lines } from "../types/OrderDetails";
import { LineItemData } from "../components/OrderReturnPage/form";
import {
OrderDetails_order,
OrderDetails_order_fulfillments_lines,
OrderDetails_order_lines
} from "../types/OrderDetails";
import {
OrderRefundData_order_fulfillments,
OrderRefundData_order_lines
@ -11,10 +20,53 @@ import {
getAllFulfillmentLinesPriceSum,
getPreviouslyRefundedPrice,
getRefundedLinesPriceSum,
getReplacedProductsAmount,
getReturnSelectedProductsAmount,
mergeRepeatedOrderLines,
OrderWithTotalAndTotalCaptured
} from "./data";
const orderBase: OrderDetails_order = {
__typename: "Order",
actions: [],
availableShippingMethods: [],
canFinalize: true,
channel: null,
billingAddress: {
__typename: "Address",
city: "Port Danielshire",
cityArea: "",
companyName: "",
country: {
__typename: "CountryDisplay",
code: "SE",
country: "Szwecja"
},
countryArea: "",
firstName: "Elizabeth",
id: "QWRkcmVzczoy",
lastName: "Vaughn",
phone: "",
postalCode: "52203",
streetAddress1: "419 Ruiz Orchard Apt. 199",
streetAddress2: ""
},
created: "2018-09-11T09:37:30.124154+00:00",
id: "T3JkZXI6MTk=",
number: "19",
paymentStatus: PaymentChargeStatusEnum.FULLY_CHARGED,
status: OrderStatus.FULFILLED,
// @ts-ignore
total: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 1215.89,
currency: "USD"
}
}
};
describe("Get previously refunded price", () => {
it("is able to calculate refunded price from order", () => {
const order: OrderWithTotalAndTotalCaptured = {
@ -397,6 +449,762 @@ describe("Get get all fulfillment lines price sum", () => {
});
});
describe("Get the total value of all replaced products", () => {
it("sums up correctly", () => {
const unfulfilledLines: OrderDetails_order_lines[] = [
{
id: "1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 2,
quantityFulfilled: 2,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
{
id: "2",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 10,
quantityFulfilled: 2,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
{
id: "3",
isShippingRequired: true,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "T-shirt",
productSku: "29810068",
quantity: 6,
quantityFulfilled: 1,
unitPrice: {
gross: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
net: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleordemoproduct_cl_boot06_1-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
}
];
const fulfilledLines: OrderDetails_order_fulfillments_lines[] = [
{
id: "4",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 20,
quantityFulfilled: 6,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
},
{
id: "5",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 25,
quantityFulfilled: 8,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
},
{
id: "6",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ3",
isShippingRequired: true,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "T-shirt",
productSku: "29810068",
quantity: 10,
quantityFulfilled: 3,
unitPrice: {
gross: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
net: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleordemoproduct_cl_boot06_1-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
},
{
id: "7",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 20,
quantityFulfilled: 6,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
},
{
id: "8",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 25,
quantityFulfilled: 8,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
}
];
const unfulfiledItemsQuantities: FormsetData<LineItemData, number> = [
{
data: { isFulfillment: false, isRefunded: false },
id: "1",
label: null,
value: 0
},
{
data: { isFulfillment: false, isRefunded: false },
id: "2",
label: null,
value: 2
},
{
data: { isFulfillment: false, isRefunded: false },
id: "3",
label: null,
value: 1
}
];
const fulfiledItemsQuantities: FormsetData<LineItemData, number> = [
{
data: { isFulfillment: true, isRefunded: false },
id: "4",
label: null,
value: 4
},
{
data: { isFulfillment: true, isRefunded: false },
id: "5",
label: null,
value: 0
},
{
data: { isFulfillment: true, isRefunded: false },
id: "6",
label: null,
value: 3
},
{
data: { isFulfillment: true, isRefunded: true },
id: "7",
label: null,
value: 4
},
{
data: { isFulfillment: true, isRefunded: true },
id: "8",
label: null,
value: 3
}
];
const itemsToBeReplaced: FormsetData<LineItemData, boolean> = [
{
data: { isFulfillment: false, isRefunded: false },
id: "1",
label: null,
value: true
},
{
data: { isFulfillment: false, isRefunded: false },
id: "2",
label: null,
value: false
},
{
data: { isFulfillment: false, isRefunded: false },
id: "3",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: false },
id: "4",
label: null,
value: false
},
{
data: { isFulfillment: true, isRefunded: false },
id: "5",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: false },
id: "6",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: true },
id: "7",
label: null,
value: false
},
{
data: { isFulfillment: true, isRefunded: true },
id: "8",
label: null,
value: true
}
];
const totalValue = getReplacedProductsAmount(
{
...orderBase,
lines: unfulfilledLines,
fulfillments: [
{
id: "#1",
fulfillmentOrder: 1,
status: FulfillmentStatus.FULFILLED,
warehouse: null,
trackingNumber: "",
lines: fulfilledLines,
__typename: "Fulfillment"
}
]
},
{
itemsToBeReplaced,
unfulfiledItemsQuantities,
fulfiledItemsQuantities
}
);
expect(totalValue).toBe(10);
});
});
describe("Get the total value of all selected products", () => {
it("sums up correctly", () => {
const unfulfilledLines: OrderDetails_order_lines[] = [
{
id: "1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 2,
quantityFulfilled: 2,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
{
id: "2",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 10,
quantityFulfilled: 2,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
{
id: "3",
isShippingRequired: true,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "T-shirt",
productSku: "29810068",
quantity: 6,
quantityFulfilled: 1,
unitPrice: {
gross: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
net: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleordemoproduct_cl_boot06_1-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
}
];
const fulfilledLines: OrderDetails_order_fulfillments_lines[] = [
{
id: "4",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 20,
quantityFulfilled: 6,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
},
{
id: "5",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
quantity: 25,
quantityFulfilled: 8,
unitPrice: {
gross: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
net: {
amount: 9.99,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleor-digital-03_2-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
},
{
id: "6",
quantity: 1,
orderLine: {
id: "T3JkZXJMaW5lOjQ3",
isShippingRequired: true,
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
__typename: "ProductVariant"
},
productName: "T-shirt",
productSku: "29810068",
quantity: 10,
quantityFulfilled: 3,
unitPrice: {
gross: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
net: {
amount: 2.5,
currency: "USD",
__typename: "Money"
},
__typename: "TaxedMoney"
},
thumbnail: {
url:
"http://localhost:8000/media/__sized__/products/saleordemoproduct_cl_boot06_1-thumbnail-255x255.png",
__typename: "Image"
},
__typename: "OrderLine"
},
__typename: "FulfillmentLine"
}
];
const unfulfiledItemsQuantities: FormsetData<null, number> = [
{
data: null,
id: "1",
label: null,
value: 0
},
{
data: null,
id: "2",
label: null,
value: 2
},
{
data: null,
id: "3",
label: null,
value: 1
}
];
const fulfiledItemsQuantities: FormsetData<null, number> = [
{
data: null,
id: "4",
label: null,
value: 4
},
{
data: null,
id: "5",
label: null,
value: 0
},
{
data: null,
id: "6",
label: null,
value: 3
}
];
const itemsToBeReplaced: FormsetData<LineItemData, boolean> = [
{
data: { isFulfillment: false, isRefunded: false },
id: "1",
label: null,
value: true
},
{
data: { isFulfillment: false, isRefunded: false },
id: "2",
label: null,
value: false
},
{
data: { isFulfillment: false, isRefunded: false },
id: "3",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: false },
id: "4",
label: null,
value: false
},
{
data: { isFulfillment: true, isRefunded: false },
id: "5",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: false },
id: "6",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: true },
id: "7",
label: null,
value: true
},
{
data: { isFulfillment: true, isRefunded: true },
id: "8",
label: null,
value: true
}
];
const totalValue = getReturnSelectedProductsAmount(
{
...orderBase,
lines: unfulfilledLines,
fulfillments: [
{
id: "#1",
fulfillmentOrder: 1,
status: FulfillmentStatus.FULFILLED,
warehouse: null,
trackingNumber: "",
lines: fulfilledLines,
__typename: "Fulfillment"
}
]
},
{
itemsToBeReplaced,
unfulfiledItemsQuantities,
fulfiledItemsQuantities
}
);
expect(totalValue).toBe(59.94);
});
});
describe("Merge repeated order lines of fulfillment lines", () => {
it("is able to merge repeated order lines and sum their quantities", () => {
const lines: OrderDetails_order_fulfillments_lines[] = [

View file

@ -1,7 +1,19 @@
import { IMoney, subtractMoney } from "@saleor/components/Money";
import { FormsetData } from "@saleor/hooks/useFormset";
import { OrderDetails_order_fulfillments_lines } from "../types/OrderDetails";
import {
LineItemData,
OrderReturnFormData
} from "../components/OrderReturnPage/form";
import {
getAllOrderFulfilledLines,
getById
} from "../components/OrderReturnPage/utils";
import {
OrderDetails_order,
OrderDetails_order_fulfillments_lines,
OrderDetails_order_lines
} from "../types/OrderDetails";
import {
OrderRefundData_order,
OrderRefundData_order_fulfillments,
@ -23,9 +35,130 @@ export function getPreviouslyRefundedPrice(
);
}
const getItemPriceAndQuantity = ({
orderLines,
itemsQuantities,
id
}: {
orderLines: OrderDetails_order_lines[];
itemsQuantities: FormsetData<LineItemData, number>;
id: string;
}) => {
const { unitPrice } = orderLines.find(getById(id));
const selectedQuantity = itemsQuantities.find(getById(id))?.value;
return { selectedQuantity, unitPrice };
};
const selectItemPriceAndQuantity = (
order: OrderDetails_order,
{
fulfiledItemsQuantities,
unfulfiledItemsQuantities
}: Partial<OrderReturnFormData>,
id: string,
isFulfillment: boolean
) =>
isFulfillment
? getItemPriceAndQuantity({
id,
itemsQuantities: fulfiledItemsQuantities,
orderLines: getAllOrderFulfilledLines(order)
})
: getItemPriceAndQuantity({
id,
itemsQuantities: unfulfiledItemsQuantities,
orderLines: order.lines
});
export const getReplacedProductsAmount = (
order: OrderDetails_order,
{
itemsToBeReplaced,
unfulfiledItemsQuantities,
fulfiledItemsQuantities
}: Partial<OrderReturnFormData>
) => {
if (!order || !itemsToBeReplaced.length) {
return 0;
}
return itemsToBeReplaced.reduce(
(
resultAmount: number,
{ id, value: isItemToBeReplaced, data: { isFulfillment, isRefunded } }
) => {
if (!isItemToBeReplaced || isRefunded) {
return resultAmount;
}
const { unitPrice, selectedQuantity } = selectItemPriceAndQuantity(
order,
{ fulfiledItemsQuantities, unfulfiledItemsQuantities },
id,
isFulfillment
);
return resultAmount + unitPrice?.gross?.amount * selectedQuantity;
},
0
);
};
export const getReturnSelectedProductsAmount = (
order: OrderDetails_order,
{ itemsToBeReplaced, unfulfiledItemsQuantities, fulfiledItemsQuantities }
) => {
if (!order) {
return 0;
}
const unfulfilledItemsValue = getPartialProductsValue({
itemsQuantities: unfulfiledItemsQuantities,
itemsToBeReplaced,
orderLines: order.lines
});
const fulfiledItemsValue = getPartialProductsValue({
itemsQuantities: fulfiledItemsQuantities,
itemsToBeReplaced,
orderLines: getAllOrderFulfilledLines(order)
});
return unfulfilledItemsValue + fulfiledItemsValue;
};
const getPartialProductsValue = ({
orderLines,
itemsQuantities,
itemsToBeReplaced
}: {
itemsToBeReplaced: FormsetData<LineItemData, boolean>;
itemsQuantities: FormsetData<LineItemData, number>;
orderLines: OrderDetails_order_lines[];
}) =>
itemsQuantities.reduce((resultAmount, { id, value: quantity }) => {
const {
value: isItemToBeReplaced,
data: { isRefunded }
} = itemsToBeReplaced.find(getById(id));
if (quantity < 1 || isItemToBeReplaced || isRefunded) {
return resultAmount;
}
const { selectedQuantity, unitPrice } = getItemPriceAndQuantity({
id,
itemsQuantities,
orderLines
});
return resultAmount + unitPrice.gross.amount * selectedQuantity;
}, 0);
export function getRefundedLinesPriceSum(
lines: OrderRefundData_order_lines[],
refundedProductQuantities: FormsetData<null, string>
refundedProductQuantities: FormsetData<null, string | number>
): number {
return lines?.reduce((sum, line) => {
const refundedLine = refundedProductQuantities.find(
@ -37,7 +170,7 @@ export function getRefundedLinesPriceSum(
export function getAllFulfillmentLinesPriceSum(
fulfillments: OrderRefundData_order_fulfillments[],
refundedFulfilledProductQuantities: FormsetData<null, string>
refundedFulfilledProductQuantities: FormsetData<null, string | number>
): number {
return fulfillments?.reduce((sum, fulfillment) => {
const fulfilmentLinesSum = fulfillment?.lines.reduce((sum, line) => {

View file

@ -50,6 +50,7 @@ import {
orderFulfillUrl,
orderListUrl,
orderRefundUrl,
orderReturnPath,
orderUrl,
OrderUrlDialog,
OrderUrlQueryParams
@ -227,6 +228,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
)}
/>
<OrderDetailsPage
onOrderReturn={() => navigate(orderReturnPath(id))}
disabled={
updateMetadataOpts.loading ||
updatePrivateMetadataOpts.loading

View file

@ -0,0 +1,113 @@
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import OrderReturnPage from "@saleor/orders/components/OrderReturnPage";
import { OrderReturnFormData } from "@saleor/orders/components/OrderReturnPage/form";
import { useOrderReturnCreateMutation } from "@saleor/orders/mutations";
import { useOrderQuery } from "@saleor/orders/queries";
import { orderUrl } from "@saleor/orders/urls";
import { OrderErrorCode } from "@saleor/types/globalTypes";
import React from "react";
import { defineMessages } from "react-intl";
import { useIntl } from "react-intl";
import ReturnFormDataParser from "./utils";
export const messages = defineMessages({
cannotRefundDescription: {
defaultMessage:
"Weve encountered a problem while refunding the products. Products were not refunded. Please try again.",
description: "order return error description when cannot refund"
},
cannotRefundTitle: {
defaultMessage: "Couldn't refund products",
description: "order return error title when cannot refund"
},
successAlert: {
defaultMessage: "Successfully returned products!",
description: "order returned success message"
}
});
interface OrderReturnProps {
orderId: string;
}
const OrderReturn: React.FC<OrderReturnProps> = ({ orderId }) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const { data, loading } = useOrderQuery({
displayLoader: true,
variables: {
id: orderId
}
});
const [returnCreate, returnCreateOpts] = useOrderReturnCreateMutation({
onCompleted: ({
orderFulfillmentReturnProducts: { errors, replaceOrder }
}) => {
if (!errors.length) {
notify({
status: "success",
text: intl.formatMessage(messages.successAlert)
});
navigateToOrder(replaceOrder?.id);
}
if (errors[0].code === OrderErrorCode.CANNOT_REFUND) {
notify({
autohide: 5000,
status: "error",
text: intl.formatMessage(messages.cannotRefundDescription),
title: intl.formatMessage(messages.cannotRefundTitle)
});
return;
}
notify({
autohide: 5000,
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
});
const handleSubmit = async (formData: OrderReturnFormData) => {
if (!data?.order) {
return;
}
const result = await returnCreate({
variables: {
id: data.order.id,
input: new ReturnFormDataParser(data.order, formData).getParsedData()
}
});
const {
data: { orderFulfillmentReturnProducts }
} = result;
return orderFulfillmentReturnProducts.errors;
};
const navigateToOrder = (id?: string) => navigate(orderUrl(id || orderId));
return (
<OrderReturnPage
errors={returnCreateOpts.data?.orderFulfillmentReturnProducts.errors}
order={data?.order}
loading={loading || returnCreateOpts.loading}
onSubmit={handleSubmit}
onBack={() => navigateToOrder()}
/>
);
};
OrderReturn.displayName = "OrderReturn";
export default OrderReturn;

View file

@ -0,0 +1,2 @@
export * from "./OrderReturn";
export { default } from "./OrderReturn";

View file

@ -0,0 +1,102 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { OrderRefundAmountCalculationMode } from "@saleor/orders/components/OrderRefundPage/form";
import {
FormsetQuantityData,
OrderReturnFormData
} from "@saleor/orders/components/OrderReturnPage/form";
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import {
OrderReturnFulfillmentLineInput,
OrderReturnLineInput,
OrderReturnProductsInput
} from "@saleor/types/globalTypes";
class ReturnFormDataParser {
private order: OrderDetails_order;
private formData: OrderReturnFormData;
constructor(order: OrderDetails_order, formData: OrderReturnFormData) {
this.order = order;
this.formData = formData;
}
public getParsedData = (): OrderReturnProductsInput => {
const {
fulfiledItemsQuantities,
unfulfiledItemsQuantities,
refundShipmentCosts
} = this.formData;
const fulfillmentLines = this.getParsedLineData<
OrderReturnFulfillmentLineInput
>(fulfiledItemsQuantities, "fulfillmentLineId");
const orderLines = this.getParsedLineData<OrderReturnLineInput>(
unfulfiledItemsQuantities,
"orderLineId"
);
return {
amountToRefund: this.getAmountToRefund(),
fulfillmentLines,
includeShippingCosts: refundShipmentCosts,
orderLines,
refund: this.getShouldRefund(orderLines, fulfillmentLines)
};
};
private getAmountToRefund = (): number | undefined =>
this.formData.amountCalculationMode ===
OrderRefundAmountCalculationMode.MANUAL
? this.formData.amount
: undefined;
private getParsedLineData = function<
T extends OrderReturnFulfillmentLineInput | OrderReturnLineInput
>(
itemsQuantities: FormsetQuantityData,
idKey: "fulfillmentLineId" | "orderLineId"
): T[] {
const { itemsToBeReplaced } = this.formData;
return itemsQuantities.reduce((result, { value: quantity, id }) => {
if (!quantity) {
return result;
}
const shouldReplace = !!itemsToBeReplaced.find(getById(id))?.value;
return [
...result,
({ [idKey]: id, quantity, replace: shouldReplace } as unknown) as T
];
}, []);
};
private getShouldRefund = (
orderLines: OrderReturnLineInput[],
fulfillmentLines: OrderReturnFulfillmentLineInput[]
) => {
if (!this.order.totalCaptured?.amount) {
return false;
}
if (!!this.getAmountToRefund()) {
return true;
}
return (
orderLines.some(ReturnFormDataParser.isLineRefundable) ||
fulfillmentLines.some(ReturnFormDataParser.isLineRefundable)
);
};
private static isLineRefundable = function<
T extends OrderReturnLineInput | OrderReturnFulfillmentLineInput
>({ replace }: T) {
return !replace;
};
}
export default ReturnFormDataParser;

View file

@ -1,11 +1,13 @@
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
import React from "react";
import { IntlProvider } from "react-intl";
import { BrowserRouter } from "react-router-dom";
import { Provider as DateProvider } from "../components/Date/DateContext";
import MessageManagerProvider from "../components/messages";
import ThemeProvider from "../components/Theme";
import { TimezoneProvider } from "../components/Timezone";
import { APP_MOUNT_URI } from "../config";
export const Decorator = storyFn => (
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
@ -18,6 +20,7 @@ export const Decorator = storyFn => (
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York">
<ThemeProvider isDefaultDark={false}>
<BrowserRouter basename={APP_MOUNT_URI}>
<MessageManagerProvider>
<div
style={{
@ -27,6 +30,7 @@ export const Decorator = storyFn => (
{storyFn()}
</div>
</MessageManagerProvider>
</BrowserRouter>
</ThemeProvider>
</TimezoneProvider>
</DateProvider>

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
onNoteAdd: undefined,
onOrderCancel: undefined,
onOrderFulfill: undefined,
onOrderReturn: () => undefined,
onPaymentCapture: undefined,
onPaymentPaid: undefined,
onPaymentRefund: undefined,

View file

@ -451,6 +451,9 @@ export enum FulfillmentStatus {
CANCELED = "CANCELED",
FULFILLED = "FULFILLED",
REFUNDED = "REFUNDED",
REFUNDED_AND_RETURNED = "REFUNDED_AND_RETURNED",
REPLACED = "REPLACED",
RETURNED = "RETURNED",
}
export enum InvoiceErrorCode {
@ -564,7 +567,6 @@ export enum OrderErrorCode {
CANNOT_CANCEL_ORDER = "CANNOT_CANCEL_ORDER",
CANNOT_DELETE = "CANNOT_DELETE",
CANNOT_REFUND = "CANNOT_REFUND",
CANNOT_REFUND_FULFILLMENT_LINE = "CANNOT_REFUND_FULFILLMENT_LINE",
CAPTURE_INACTIVE_PAYMENT = "CAPTURE_INACTIVE_PAYMENT",
CHANNEL_INACTIVE = "CHANNEL_INACTIVE",
DUPLICATED_INPUT_ITEM = "DUPLICATED_INPUT_ITEM",
@ -572,7 +574,7 @@ export enum OrderErrorCode {
GRAPHQL_ERROR = "GRAPHQL_ERROR",
INSUFFICIENT_STOCK = "INSUFFICIENT_STOCK",
INVALID = "INVALID",
INVALID_REFUND_QUANTITY = "INVALID_REFUND_QUANTITY",
INVALID_QUANTITY = "INVALID_QUANTITY",
NOT_AVAILABLE_IN_CHANNEL = "NOT_AVAILABLE_IN_CHANNEL",
NOT_EDITABLE = "NOT_EDITABLE",
NOT_FOUND = "NOT_FOUND",
@ -607,13 +609,16 @@ export enum OrderEventsEnum {
CONFIRMED = "CONFIRMED",
DRAFT_ADDED_PRODUCTS = "DRAFT_ADDED_PRODUCTS",
DRAFT_CREATED = "DRAFT_CREATED",
DRAFT_CREATED_FROM_REPLACE = "DRAFT_CREATED_FROM_REPLACE",
DRAFT_REMOVED_PRODUCTS = "DRAFT_REMOVED_PRODUCTS",
EMAIL_SENT = "EMAIL_SENT",
EXTERNAL_SERVICE_NOTIFICATION = "EXTERNAL_SERVICE_NOTIFICATION",
FULFILLMENT_CANCELED = "FULFILLMENT_CANCELED",
FULFILLMENT_FULFILLED_ITEMS = "FULFILLMENT_FULFILLED_ITEMS",
FULFILLMENT_REFUNDED = "FULFILLMENT_REFUNDED",
FULFILLMENT_REPLACED = "FULFILLMENT_REPLACED",
FULFILLMENT_RESTOCKED_ITEMS = "FULFILLMENT_RESTOCKED_ITEMS",
FULFILLMENT_RETURNED = "FULFILLMENT_RETURNED",
INVOICE_GENERATED = "INVOICE_GENERATED",
INVOICE_REQUESTED = "INVOICE_REQUESTED",
INVOICE_SENT = "INVOICE_SENT",
@ -621,6 +626,7 @@ export enum OrderEventsEnum {
NOTE_ADDED = "NOTE_ADDED",
ORDER_FULLY_PAID = "ORDER_FULLY_PAID",
ORDER_MARKED_AS_PAID = "ORDER_MARKED_AS_PAID",
ORDER_REPLACEMENT_CREATED = "ORDER_REPLACEMENT_CREATED",
OTHER = "OTHER",
OVERSOLD_ITEMS = "OVERSOLD_ITEMS",
PAYMENT_AUTHORIZED = "PAYMENT_AUTHORIZED",
@ -651,6 +657,8 @@ export enum OrderStatus {
DRAFT = "DRAFT",
FULFILLED = "FULFILLED",
PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED",
PARTIALLY_RETURNED = "PARTIALLY_RETURNED",
RETURNED = "RETURNED",
UNCONFIRMED = "UNCONFIRMED",
UNFULFILLED = "UNFULFILLED",
}
@ -1377,6 +1385,26 @@ export interface OrderRefundProductsInput {
includeShippingCosts?: boolean | null;
}
export interface OrderReturnFulfillmentLineInput {
fulfillmentLineId: string;
quantity: number;
replace?: boolean | null;
}
export interface OrderReturnLineInput {
orderLineId: string;
quantity: number;
replace?: boolean | null;
}
export interface OrderReturnProductsInput {
orderLines?: OrderReturnLineInput[] | null;
fulfillmentLines?: OrderReturnFulfillmentLineInput[] | null;
amountToRefund?: any | null;
includeShippingCosts?: boolean | null;
refund?: boolean | null;
}
export interface OrderSettingsUpdateInput {
automaticallyConfirmAllNewOrders: boolean;
}