diff --git a/.changeset/angry-buckets-marry.md b/.changeset/angry-buckets-marry.md new file mode 100644 index 00000000000..5720c9e429a --- /dev/null +++ b/.changeset/angry-buckets-marry.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Send order lines in orderGrantRefundCreate mutation diff --git a/introspection.json b/introspection.json index 527d79c8d67..449983419d1 100644 --- a/introspection.json +++ b/introspection.json @@ -17703,7 +17703,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether checkout prices should include taxes when displayed in a storefront.\n\nAdded in Saleor 3.9.", + "description": "Determines whether displayed prices should include taxes.\n\nAdded in Saleor 3.9.", "args": [], "type": { "kind": "NON_NULL", @@ -30049,7 +30049,7 @@ { "kind": "OBJECT", "name": "Domain", - "description": "Represents shop's domain.", + "description": "Represents API domain.", "fields": [ { "name": "host", @@ -30085,7 +30085,7 @@ }, { "name": "url", - "description": "Shop's absolute URL.", + "description": "The absolute URL of the API.", "args": [], "type": { "kind": "NON_NULL", @@ -62390,7 +62390,7 @@ }, { "name": "stockBulkUpdate", - "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.", + "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.\n\nTriggers the following webhook events:\n- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated.", "args": [ { "name": "errorPolicy", @@ -65492,7 +65492,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether checkout prices should include taxes when displayed in a storefront.\n\nAdded in Saleor 3.9.", + "description": "Determines whether displayed prices should include taxes.\n\nAdded in Saleor 3.9.", "args": [], "type": { "kind": "NON_NULL", @@ -67546,7 +67546,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether checkout prices should include taxes, when displayed in a storefront.", + "description": "Determines whether displayed prices should include taxes.", "type": { "kind": "SCALAR", "name": "Boolean", @@ -73479,6 +73479,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "saleId", + "description": "Denormalized sale ID, set when order line is created for a product variant that is on sale.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "taxClass", "description": "Denormalized tax class of the product in this order line.\n\nAdded in Saleor 3.9.\n\nRequires one of the following permissions: AUTHENTICATED_STAFF_USER, AUTHENTICATED_APP.", @@ -73783,6 +73795,18 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "voucherCode", + "description": "Voucher code that was used for this order line.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -80425,6 +80449,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "partial", + "description": "Informs whether this is a partial payment.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "paymentMethodType", "description": "Type of method used for payment.", @@ -80527,6 +80567,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "pspReference", + "description": "PSP reference of the payment.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "token", "description": "Unique token associated with a payment.", @@ -91714,7 +91766,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether this product's price displayed in a storefront should include taxes.\n\nAdded in Saleor 3.9.", + "description": "Determines whether displayed prices should include taxes.\n\nAdded in Saleor 3.9.", "args": [], "type": { "kind": "NON_NULL", @@ -118724,7 +118776,7 @@ { "kind": "OBJECT", "name": "StockBulkUpdate", - "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.", + "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.\n\nTriggers the following webhook events:\n- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated.", "fields": [ { "name": "count", @@ -121043,7 +121095,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes.", + "description": "Determines whether displayed prices should include taxes.", "args": [], "type": { "kind": "NON_NULL", @@ -121486,7 +121538,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes for this country.", + "description": "Determines whether displayed prices should include taxes for this country.", "args": [], "type": { "kind": "NON_NULL", @@ -121558,7 +121610,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes for this country.", + "description": "Determines whether displayed prices should include taxes for this country.", "type": { "kind": "NON_NULL", "name": null, @@ -121766,7 +121818,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes.", + "description": "Determines whether displayed prices should include taxes.", "type": { "kind": "SCALAR", "name": "Boolean", diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 9234151da3f..f1898222395 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -7373,6 +7373,9 @@ "context": "Add filter button text", "string": "+ Add filter" }, + "o/4OCR": { + "string": "Shipping has already been refunded" + }, "o5KXAN": { "context": "delete webhook", "string": "Are you sure you want to delete {name}?" diff --git a/schema.graphql b/schema.graphql index 83f144a7402..508fa0cc98f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4482,7 +4482,7 @@ type Checkout implements Node & ObjectWithMetadata { discountName: String """ - Determines whether checkout prices should include taxes when displayed in a storefront. + Determines whether displayed prices should include taxes. Added in Saleor 3.9. """ @@ -7351,7 +7351,7 @@ enum DistanceUnitsEnum { YD } -"""Represents shop's domain.""" +"""Represents API domain.""" type Domain { """The host name of the domain.""" host: String! @@ -7359,7 +7359,7 @@ type Domain { """Inform if SSL is enabled.""" sslEnabled: Boolean! - """Shop's absolute URL.""" + """The absolute URL of the API.""" url: String! } @@ -15898,6 +15898,9 @@ type Mutation { Note: this API is currently in Feature Preview and can be subject to changes at later point. Requires one of the following permissions: MANAGE_PRODUCTS. + + Triggers the following webhook events: + - PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated. """ stockBulkUpdate( """Policies of error handling. DEFAULT: REJECT_EVERYTHING""" @@ -16656,7 +16659,7 @@ type Order implements Node & ObjectWithMetadata { discounts: [OrderDiscount!]! """ - Determines whether checkout prices should include taxes when displayed in a storefront. + Determines whether displayed prices should include taxes. Added in Saleor 3.9. """ @@ -17179,9 +17182,7 @@ input OrderBulkCreateInput { """List of discounts.""" discounts: [OrderDiscountCommonInput!] - """ - Determines whether checkout prices should include taxes, when displayed in a storefront. - """ + """Determines whether displayed prices should include taxes.""" displayGrossPrices: Boolean """External ID of the order.""" @@ -18538,6 +18539,13 @@ type OrderLine implements Node & ObjectWithMetadata { """ quantityToFulfill: Int! + """ + Denormalized sale ID, set when order line is created for a product variant that is on sale. + + Added in Saleor 3.14. + """ + saleId: ID + """ Denormalized tax class of the product in this order line. @@ -18617,6 +18625,13 @@ type OrderLine implements Node & ObjectWithMetadata { """ variant: ProductVariant variantName: String! + + """ + Voucher code that was used for this order line. + + Added in Saleor 3.14. + """ + voucherCode: String } input OrderLineCreateInput { @@ -20228,6 +20243,13 @@ type Payment implements Node & ObjectWithMetadata { """Order associated with a payment.""" order: Order + """ + Informs whether this is a partial payment. + + Added in Saleor 3.14. + """ + partial: Boolean! + """Type of method used for payment.""" paymentMethodType: String! @@ -20250,6 +20272,13 @@ type Payment implements Node & ObjectWithMetadata { """ privateMetafields(keys: [String!]): Metadata + """ + PSP reference of the payment. + + Added in Saleor 3.14. + """ + pspReference: String + """Unique token associated with a payment.""" token: String! @@ -23006,7 +23035,7 @@ type ProductPricingInfo { discountLocalCurrency: TaxedMoney @deprecated(reason: "This field will be removed in Saleor 4.0. Always returns `null`.") """ - Determines whether this product's price displayed in a storefront should include taxes. + Determines whether displayed prices should include taxes. Added in Saleor 3.9. """ @@ -30045,6 +30074,9 @@ Added in Saleor 3.13. Note: this API is currently in Feature Preview and can be subject to changes at later point. Requires one of the following permissions: MANAGE_PRODUCTS. + +Triggers the following webhook events: +- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated. """ type StockBulkUpdate { """Returns how many objects were updated.""" @@ -30602,9 +30634,7 @@ type TaxConfiguration implements Node & ObjectWithMetadata { """List of country-specific exceptions in tax configuration.""" countries: [TaxConfigurationPerCountry!]! - """ - Determines whether prices displayed in a storefront should include taxes. - """ + """Determines whether displayed prices should include taxes.""" displayGrossPrices: Boolean! """The ID of the object.""" @@ -30693,7 +30723,7 @@ type TaxConfigurationPerCountry { country: CountryDisplay! """ - Determines whether prices displayed in a storefront should include taxes for this country. + Determines whether displayed prices should include taxes for this country. """ displayGrossPrices: Boolean! @@ -30711,7 +30741,7 @@ input TaxConfigurationPerCountryInput { countryCode: CountryCode! """ - Determines whether prices displayed in a storefront should include taxes for this country. + Determines whether displayed prices should include taxes for this country. """ displayGrossPrices: Boolean! @@ -30761,9 +30791,7 @@ input TaxConfigurationUpdateInput { """Determines whether taxes are charged in the given channel.""" chargeTaxes: Boolean - """ - Determines whether prices displayed in a storefront should include taxes. - """ + """Determines whether displayed prices should include taxes.""" displayGrossPrices: Boolean """Determines whether prices are entered with the tax included.""" diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts index 2307591b82b..cd676dfb047 100644 --- a/src/fragments/errors.ts +++ b/src/fragments/errors.ts @@ -565,6 +565,12 @@ export const orderGrantRefundCreateErrorFragment = gql` field message code + lines { + field + message + code + lineId + } } `; @@ -573,5 +579,17 @@ export const orderGrantRefundUpdateErrorFragment = gql` field message code + addLines { + field + message + code + lineId + } + removeLines { + field + message + code + lineId + } } `; diff --git a/src/fragments/orders.ts b/src/fragments/orders.ts index d86b5c61c96..b7f749c0af8 100644 --- a/src/fragments/orders.ts +++ b/src/fragments/orders.ts @@ -622,6 +622,24 @@ export const orderLineGrantRefund = gql` } `; +export const orderDetailsGrantedRefund = gql` + fragment OrderDetailsGrantedRefund on OrderGrantedRefund { + id + reason + amount { + ...Money + } + shippingCostsIncluded + lines { + id + quantity + orderLine { + ...OrderLine + } + } + } +`; + export const grantRefundFulfillment = gql` fragment OrderFulfillmentGrantRefund on Fulfillment { id @@ -657,5 +675,8 @@ export const fragmentOrderDetailsGrantRefund = gql` ...Money } } + grantedRefunds { + ...OrderDetailsGrantedRefund + } } `; diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index a3ee1eb5c69..b735fb5fe52 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -1119,6 +1119,12 @@ export const OrderGrantRefundCreateErrorFragmentDoc = gql` field message code + lines { + field + message + code + lineId + } } `; export const OrderGrantRefundUpdateErrorFragmentDoc = gql` @@ -1126,6 +1132,18 @@ export const OrderGrantRefundUpdateErrorFragmentDoc = gql` field message code + addLines { + field + message + code + lineId + } + removeLines { + field + message + code + lineId + } } `; export const GiftCardsSettingsFragmentDoc = gql` @@ -1990,6 +2008,24 @@ export const OrderFulfillmentGrantRefundFragmentDoc = gql` } } ${OrderLineGrantRefundFragmentDoc}`; +export const OrderDetailsGrantedRefundFragmentDoc = gql` + fragment OrderDetailsGrantedRefund on OrderGrantedRefund { + id + reason + amount { + ...Money + } + shippingCostsIncluded + lines { + id + quantity + orderLine { + ...OrderLine + } + } +} + ${MoneyFragmentDoc} +${OrderLineFragmentDoc}`; export const OrderDetailsGrantRefundFragmentDoc = gql` fragment OrderDetailsGrantRefund on Order { id @@ -2010,10 +2046,14 @@ export const OrderDetailsGrantRefundFragmentDoc = gql` ...Money } } + grantedRefunds { + ...OrderDetailsGrantedRefund + } } ${OrderLineGrantRefundFragmentDoc} ${OrderFulfillmentGrantRefundFragmentDoc} -${MoneyFragmentDoc}`; +${MoneyFragmentDoc} +${OrderDetailsGrantedRefundFragmentDoc}`; export const PageTypeFragmentDoc = gql` fragment PageType on PageType { id @@ -10498,8 +10538,11 @@ export type OrderTransactionRequestActionMutationHookResult = ReturnType; export type OrderTransactionRequestActionMutationOptions = Apollo.BaseMutationOptions; export const OrderGrantRefundAddDocument = gql` - mutation OrderGrantRefundAdd($orderId: ID!, $amount: Decimal!, $reason: String) { - orderGrantRefundCreate(id: $orderId, input: {amount: $amount, reason: $reason}) { + mutation OrderGrantRefundAdd($orderId: ID!, $amount: Decimal, $reason: String, $lines: [OrderGrantRefundCreateLineInput!], $grantRefundForShipping: Boolean) { + orderGrantRefundCreate( + id: $orderId + input: {amount: $amount, reason: $reason, lines: $lines, grantRefundForShipping: $grantRefundForShipping} + ) { errors { ...OrderGrantRefundCreateError } @@ -10524,6 +10567,8 @@ export type OrderGrantRefundAddMutationFn = Apollo.MutationFunction; export type OrderGrantRefundAddMutationOptions = Apollo.BaseMutationOptions; export const OrderGrantRefundEditDocument = gql` - mutation OrderGrantRefundEdit($refundId: ID!, $amount: Decimal!, $reason: String) { - orderGrantRefundUpdate(id: $refundId, input: {amount: $amount, reason: $reason}) { + mutation OrderGrantRefundEdit($refundId: ID!, $amount: Decimal, $reason: String, $addLines: [OrderGrantRefundUpdateLineAddInput!], $removeLines: [ID!], $grantRefundForShipping: Boolean) { + orderGrantRefundUpdate( + id: $refundId + input: {amount: $amount, reason: $reason, addLines: $addLines, removeLines: $removeLines, grantRefundForShipping: $grantRefundForShipping} + ) { errors { ...OrderGrantRefundUpdateError } @@ -10561,6 +10609,9 @@ export type OrderGrantRefundEditMutationFn = Apollo.MutationFunction | FieldReadFunction, reason?: FieldPolicy | FieldReadFunction }; -export type OrderLineKeySpecifier = ('allocations' | 'digitalContentUrl' | 'id' | 'isShippingRequired' | 'metadata' | 'metafield' | 'metafields' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'productName' | 'productSku' | 'productVariantId' | 'quantity' | 'quantityFulfilled' | 'quantityToFulfill' | 'taxClass' | 'taxClassMetadata' | 'taxClassName' | 'taxClassPrivateMetadata' | 'taxRate' | 'thumbnail' | 'totalPrice' | 'translatedProductName' | 'translatedVariantName' | 'undiscountedTotalPrice' | 'undiscountedUnitPrice' | 'unitDiscount' | 'unitDiscountReason' | 'unitDiscountType' | 'unitDiscountValue' | 'unitPrice' | 'variant' | 'variantName' | OrderLineKeySpecifier)[]; +export type OrderLineKeySpecifier = ('allocations' | 'digitalContentUrl' | 'id' | 'isShippingRequired' | 'metadata' | 'metafield' | 'metafields' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'productName' | 'productSku' | 'productVariantId' | 'quantity' | 'quantityFulfilled' | 'quantityToFulfill' | 'saleId' | 'taxClass' | 'taxClassMetadata' | 'taxClassName' | 'taxClassPrivateMetadata' | 'taxRate' | 'thumbnail' | 'totalPrice' | 'translatedProductName' | 'translatedVariantName' | 'undiscountedTotalPrice' | 'undiscountedUnitPrice' | 'unitDiscount' | 'unitDiscountReason' | 'unitDiscountType' | 'unitDiscountValue' | 'unitPrice' | 'variant' | 'variantName' | 'voucherCode' | OrderLineKeySpecifier)[]; export type OrderLineFieldPolicy = { allocations?: FieldPolicy | FieldReadFunction, digitalContentUrl?: FieldPolicy | FieldReadFunction, @@ -3362,6 +3362,7 @@ export type OrderLineFieldPolicy = { quantity?: FieldPolicy | FieldReadFunction, quantityFulfilled?: FieldPolicy | FieldReadFunction, quantityToFulfill?: FieldPolicy | FieldReadFunction, + saleId?: FieldPolicy | FieldReadFunction, taxClass?: FieldPolicy | FieldReadFunction, taxClassMetadata?: FieldPolicy | FieldReadFunction, taxClassName?: FieldPolicy | FieldReadFunction, @@ -3379,7 +3380,8 @@ export type OrderLineFieldPolicy = { unitDiscountValue?: FieldPolicy | FieldReadFunction, unitPrice?: FieldPolicy | FieldReadFunction, variant?: FieldPolicy | FieldReadFunction, - variantName?: FieldPolicy | FieldReadFunction + variantName?: FieldPolicy | FieldReadFunction, + voucherCode?: FieldPolicy | FieldReadFunction }; export type OrderLineDeleteKeySpecifier = ('errors' | 'order' | 'orderErrors' | 'orderLine' | OrderLineDeleteKeySpecifier)[]; export type OrderLineDeleteFieldPolicy = { @@ -3759,7 +3761,7 @@ export type PasswordChangeFieldPolicy = { errors?: FieldPolicy | FieldReadFunction, user?: FieldPolicy | FieldReadFunction }; -export type PaymentKeySpecifier = ('actions' | 'availableCaptureAmount' | 'availableRefundAmount' | 'capturedAmount' | 'chargeStatus' | 'checkout' | 'created' | 'creditCard' | 'customerIpAddress' | 'gateway' | 'id' | 'isActive' | 'metadata' | 'metafield' | 'metafields' | 'modified' | 'order' | 'paymentMethodType' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'token' | 'total' | 'transactions' | PaymentKeySpecifier)[]; +export type PaymentKeySpecifier = ('actions' | 'availableCaptureAmount' | 'availableRefundAmount' | 'capturedAmount' | 'chargeStatus' | 'checkout' | 'created' | 'creditCard' | 'customerIpAddress' | 'gateway' | 'id' | 'isActive' | 'metadata' | 'metafield' | 'metafields' | 'modified' | 'order' | 'partial' | 'paymentMethodType' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'pspReference' | 'token' | 'total' | 'transactions' | PaymentKeySpecifier)[]; export type PaymentFieldPolicy = { actions?: FieldPolicy | FieldReadFunction, availableCaptureAmount?: FieldPolicy | FieldReadFunction, @@ -3778,10 +3780,12 @@ export type PaymentFieldPolicy = { metafields?: FieldPolicy | FieldReadFunction, modified?: FieldPolicy | FieldReadFunction, order?: FieldPolicy | FieldReadFunction, + partial?: FieldPolicy | FieldReadFunction, paymentMethodType?: FieldPolicy | FieldReadFunction, privateMetadata?: FieldPolicy | FieldReadFunction, privateMetafield?: FieldPolicy | FieldReadFunction, privateMetafields?: FieldPolicy | FieldReadFunction, + pspReference?: FieldPolicy | FieldReadFunction, token?: FieldPolicy | FieldReadFunction, total?: FieldPolicy | FieldReadFunction, transactions?: FieldPolicy | FieldReadFunction diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 62b608f2fb2..4d1f7e9adbd 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -3709,7 +3709,7 @@ export type OrderBulkCreateInput = { deliveryMethod?: InputMaybe; /** List of discounts. */ discounts?: InputMaybe>; - /** Determines whether checkout prices should include taxes, when displayed in a storefront. */ + /** Determines whether displayed prices should include taxes. */ displayGrossPrices?: InputMaybe; /** External ID of the order. */ externalReference?: InputMaybe; @@ -6706,7 +6706,7 @@ export type TaxConfigurationPerCountryInput = { chargeTaxes: Scalars['Boolean']; /** Country in which this configuration applies. */ countryCode: CountryCode; - /** Determines whether prices displayed in a storefront should include taxes for this country. */ + /** Determines whether displayed prices should include taxes for this country. */ displayGrossPrices: Scalars['Boolean']; /** A country-specific strategy to use for tax calculation. Taxes can be calculated either using user-defined flat rates or with a tax app. If not provided, use the value from the channel's tax configuration. */ taxCalculationStrategy?: InputMaybe; @@ -6723,7 +6723,7 @@ export enum TaxConfigurationUpdateErrorCode { export type TaxConfigurationUpdateInput = { /** Determines whether taxes are charged in the given channel. */ chargeTaxes?: InputMaybe; - /** Determines whether prices displayed in a storefront should include taxes. */ + /** Determines whether displayed prices should include taxes. */ displayGrossPrices?: InputMaybe; /** Determines whether prices are entered with the tax included. */ pricesEnteredWithTax?: InputMaybe; @@ -9739,9 +9739,9 @@ export type TransactionRequestActionErrorFragment = { __typename: 'TransactionRe export type TransactionCreateErrorFragment = { __typename: 'TransactionCreateError', field: string | null, message: string | null, code: TransactionCreateErrorCode }; -export type OrderGrantRefundCreateErrorFragment = { __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode }; +export type OrderGrantRefundCreateErrorFragment = { __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode, lines: Array<{ __typename: 'OrderGrantRefundCreateLineError', field: string | null, message: string | null, code: OrderGrantRefundCreateLineErrorCode, lineId: string }> | null }; -export type OrderGrantRefundUpdateErrorFragment = { __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode }; +export type OrderGrantRefundUpdateErrorFragment = { __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode, addLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null, removeLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null }; export type FileFragment = { __typename: 'File', url: string, contentType: string | null }; @@ -9873,9 +9873,11 @@ export type OrderGrantedRefundFragment = { __typename: 'OrderGrantedRefund', id: export type OrderLineGrantRefundFragment = { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }; +export type OrderDetailsGrantedRefundFragment = { __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }; + export type OrderFulfillmentGrantRefundFragment = { __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }; -export type OrderDetailsGrantRefundFragment = { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }; +export type OrderDetailsGrantRefundFragment = { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }> }; export type PageInfoFragment = { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null }; @@ -10501,21 +10503,26 @@ export type OrderTransactionRequestActionMutation = { __typename: 'Mutation', tr export type OrderGrantRefundAddMutationVariables = Exact<{ orderId: Scalars['ID']; - amount: Scalars['Decimal']; + amount?: InputMaybe; reason?: InputMaybe; + lines?: InputMaybe | OrderGrantRefundCreateLineInput>; + grantRefundForShipping?: InputMaybe; }>; -export type OrderGrantRefundAddMutation = { __typename: 'Mutation', orderGrantRefundCreate: { __typename: 'OrderGrantRefundCreate', errors: Array<{ __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode }> } | null }; +export type OrderGrantRefundAddMutation = { __typename: 'Mutation', orderGrantRefundCreate: { __typename: 'OrderGrantRefundCreate', errors: Array<{ __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode, lines: Array<{ __typename: 'OrderGrantRefundCreateLineError', field: string | null, message: string | null, code: OrderGrantRefundCreateLineErrorCode, lineId: string }> | null }> } | null }; export type OrderGrantRefundEditMutationVariables = Exact<{ refundId: Scalars['ID']; - amount: Scalars['Decimal']; + amount?: InputMaybe; reason?: InputMaybe; + addLines?: InputMaybe | OrderGrantRefundUpdateLineAddInput>; + removeLines?: InputMaybe | Scalars['ID']>; + grantRefundForShipping?: InputMaybe; }>; -export type OrderGrantRefundEditMutation = { __typename: 'Mutation', orderGrantRefundUpdate: { __typename: 'OrderGrantRefundUpdate', errors: Array<{ __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode }> } | null }; +export type OrderGrantRefundEditMutation = { __typename: 'Mutation', orderGrantRefundUpdate: { __typename: 'OrderGrantRefundUpdate', errors: Array<{ __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode, addLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null, removeLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null }> } | null }; export type OrderSendRefundMutationVariables = Exact<{ amount: Scalars['PositiveDecimal']; @@ -10591,14 +10598,14 @@ export type OrderDetailsGrantRefundQueryVariables = Exact<{ }>; -export type OrderDetailsGrantRefundQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }; +export type OrderDetailsGrantRefundQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }> } | null }; export type OrderDetailsGrantRefundEditQueryVariables = Exact<{ id: Scalars['ID']; }>; -export type OrderDetailsGrantRefundEditQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, amount: { __typename: 'Money', amount: number, currency: string } }>, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }; +export type OrderDetailsGrantRefundEditQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }> } | null }; export type OrderFulfillDataQueryVariables = Exact<{ orderId: Scalars['ID']; diff --git a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx index ed26745e0d0..64007e790c3 100644 --- a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx +++ b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx @@ -220,6 +220,7 @@ const props: OrderGrantRefundPageProps = { __typename: "Fulfillment", }, ], + grantedRefunds: [], shippingPrice: { gross: { amount: 85.23, diff --git a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx index 588d33801cc..068314550a0 100644 --- a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx +++ b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx @@ -1,18 +1,25 @@ // @ts-strict-ignore import { TopNav } from "@dashboard/components/AppLayout/TopNav"; -import CardSpacer from "@dashboard/components/CardSpacer"; +import { DashboardCard } from "@dashboard/components/Card"; import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { DetailPageLayout } from "@dashboard/components/Layouts"; -import Skeleton from "@dashboard/components/Skeleton"; -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; +import { formatMoneyAmount } from "@dashboard/components/Money"; +import PriceField from "@dashboard/components/PriceField"; +import Savebar from "@dashboard/components/Savebar"; +import { + OrderDetailsGrantedRefundFragment, + OrderDetailsGrantRefundFragment, +} from "@dashboard/graphql"; +import useLocale from "@dashboard/hooks/useLocale"; +import useNavigator from "@dashboard/hooks/useNavigator"; import { orderUrl } from "@dashboard/orders/urls"; -import { Card, CardContent, TextField, Typography } from "@material-ui/core"; -import { Text } from "@saleor/macaw-ui-next"; -import React from "react"; +import { Box, Input, Skeleton, Text } from "@saleor/macaw-ui-next"; +import React, { useEffect, useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { getOrderTitleMessage } from "../OrderCardTitle/utils"; -import { ProductsCard, RefundCard } from "./components"; +import { ProductsCard } from "./components/ProductCard"; +import { ShippingIncluded } from "./components/ShippingInluded"; import { GrantRefundContext } from "./context"; import { OrderGrantRefundFormData, useGrantRefundForm } from "./form"; import { grantRefundPageMessages } from "./messages"; @@ -21,8 +28,14 @@ import { grantRefundDefaultState, grantRefundReducer, } from "./reducer"; -import { useStyles } from "./styles"; -import { calculateTotalPrice, getFulfilmentSubtitle } from "./utils"; +import { + calculateCanRefundShipping, + calculateTotalPrice, + getFulfilmentSubtitle, + getGrantedRefundData, + getRefundAmountValue, + prepareLineData, +} from "./utils"; export interface OrderGrantRefundPageProps { order: OrderDetailsGrantRefundFragment; @@ -30,7 +43,7 @@ export interface OrderGrantRefundPageProps { submitState: ConfirmButtonTransitionState; onSubmit: (data: OrderGrantRefundFormData) => void; isEdit?: boolean; - initialData?: OrderGrantRefundFormData; + initialData?: OrderDetailsGrantedRefundFragment; } const OrderGrantRefundPage: React.FC = ({ @@ -41,36 +54,68 @@ const OrderGrantRefundPage: React.FC = ({ isEdit, initialData, }) => { - const classes = useStyles(); const intl = useIntl(); + const { locale } = useLocale(); + const navigate = useNavigator(); + + const grantedRefund = useMemo( + () => getGrantedRefundData(initialData), + [initialData], + ); const unfulfilledLines = (order?.lines ?? []).filter( line => line.quantityToFulfill > 0, ); - const [state, dispatch] = React.useReducer( grantRefundReducer, grantRefundDefaultState, ); - React.useEffect(() => { + useEffect(() => { + if (grantedRefund) { + dispatch({ + type: "setRefundShipping", + refundShipping: grantedRefund.grantRefundForShipping, + }); + } + }, [grantedRefund]); + + useEffect(() => { if (order?.id) { dispatch({ type: "initState", - state: getGrantRefundReducerInitialState(order), + state: getGrantRefundReducerInitialState(order, initialData), }); } - }, [order]); + }, [order, initialData]); - const { set, change, data, submit, setIsDirty } = useGrantRefundForm({ - onSubmit, - initialData, - }); + const lines = prepareLineData(state.lines); + const canRefundShipping = calculateCanRefundShipping( + grantedRefund, + order?.grantedRefunds, + ); - const amount = parseFloat(data.amount); - const submitDisabled = Number.isNaN(amount) || amount <= 0; + const { set, change, data, submit, setIsDirty, isFormDirty } = + useGrantRefundForm({ + onSubmit, + grantedRefund, + lines, + // Send grantRefundForShipping only when it's different than the one + grantRefundForShipping: + grantedRefund?.grantRefundForShipping === state.refundShipping + ? undefined + : state.refundShipping, + }); const totalSelectedPrice = calculateTotalPrice(state, order); + const amountValue = getRefundAmountValue({ + isEditedRefundAmount: grantedRefund !== undefined, + isAmountInputDirty: isFormDirty.amount, + refundAmount: Number(data.amount), + totalCalulatedPrice: totalSelectedPrice, + }); + + const currency = order?.total?.gross?.currency ?? ""; const handleSubmit = (e: React.FormEvent) => { e.stopPropagation(); @@ -78,8 +123,22 @@ const OrderGrantRefundPage: React.FC = ({ submit(); }; + const getRefundAmountDisplayValue = () => { + if (isFormDirty) { + return amountValue.toString(); + } + + return formatMoneyAmount( + { + amount: amountValue, + currency, + }, + locale, + ); + }; + return ( - + = ({ /> } > -
+ { @@ -103,81 +162,108 @@ const OrderGrantRefundPage: React.FC = ({ }} > - - - + + + - - - -
- {loading && } - - } - lines={unfulfilledLines} - /> - {order?.fulfillments?.map?.(fulfillment => ( - - {getFulfilmentSubtitle(order, fulfillment)} - - } - lines={fulfillment.lines.map( - ({ orderLine, id, quantity }) => ({ - ...orderLine, - id, - quantity, - }), - )} + + {loading ? ( + + ) : ( + <> + + } + lines={unfulfilledLines} + /> + + {order?.fulfillments?.map?.(fulfillment => ( + + {getFulfilmentSubtitle(order, fulfillment)} + + } + lines={fulfillment.lines.map( + ({ orderLine, id, quantity }) => { + return { + ...orderLine, + id, + quantity, + }; + }, + )} + /> + ))} + + )} + + - ))} - - - + + + + - - -
+ + +
- - -
+ navigate(orderUrl(order?.id))} + onSubmit={submit} + state={submitState} + disabled={loading} + />
); }; diff --git a/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx b/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx index 19babbcf8aa..59677499e9c 100644 --- a/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx +++ b/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx @@ -1,18 +1,10 @@ // @ts-strict-ignore -import { Button } from "@dashboard/components/Button"; -import CardTitle from "@dashboard/components/CardTitle"; import TableCellAvatar from "@dashboard/components/TableCellAvatar"; import TableRowLink from "@dashboard/components/TableRowLink"; import { OrderLineGrantRefundFragment } from "@dashboard/graphql"; import { renderCollection } from "@dashboard/misc"; -import { - Card, - Table, - TableBody, - TableCell, - TableHead, - TextField, -} from "@material-ui/core"; +import { Table, TableBody, TableCell, TableHead } from "@material-ui/core"; +import { Box, Button, Input, Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; @@ -48,35 +40,36 @@ export const ProductsCard: React.FC = ({ type: "setQuantity", lineId: line.id, amount: value, + unitPrice: line.unitPrice.gross.amount, }); }; const handleSetMaxQuanity = () => { dispatch({ type: "setMaxQuantity", - lineIds: lines.map(line => line.id), + lines: lines.map(line => ({ + id: line.id, + quantity: state.lines.get(line.id)?.availableQuantity ?? 0, + unitPrice: line.unitPrice.gross.amount, + })), }); }; return ( - - - {title} - {subtitle} - - } - toolbar={ - - } - > + <> + + + {title} + {subtitle} + + + @@ -92,46 +85,49 @@ export const ProductsCard: React.FC = ({ {renderCollection( lines, - line => ( - - -
- {line?.productName} - - {line.variantName} - -
-
- - {line.quantity} - - - - / {line?.quantity} - - ), - }} - /> - -
- ), + line => { + const stateLine = state.lines.get(line.id); + + return ( + + +
+ {line?.productName} + {line.variantName} +
+
+ + {line.quantity} + + + + / {stateLine?.availableQuantity} + + ) + } + /> + +
+ ); + }, () => ( @@ -145,6 +141,6 @@ export const ProductsCard: React.FC = ({ )}
-
+ ); }; diff --git a/src/orders/components/OrderGrantRefundPage/components/RefundCard.tsx b/src/orders/components/OrderGrantRefundPage/components/RefundCard.tsx deleted file mode 100644 index 3729256e8fc..00000000000 --- a/src/orders/components/OrderGrantRefundPage/components/RefundCard.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import CardTitle from "@dashboard/components/CardTitle"; -import Checkbox from "@dashboard/components/Checkbox"; -import { - ConfirmButton, - ConfirmButtonTransitionState, -} from "@dashboard/components/ConfirmButton"; -import { formatMoneyAmount } from "@dashboard/components/Money"; -import PriceField from "@dashboard/components/PriceField"; -import Skeleton from "@dashboard/components/Skeleton"; -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; -import useLocale from "@dashboard/hooks/useLocale"; -import { buttonMessages } from "@dashboard/intl"; -import { Card, CardContent, Typography } from "@material-ui/core"; -import { useId } from "@reach/auto-id"; -import { Button } from "@saleor/macaw-ui-next"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -import { useGrantRefundContext } from "../context"; -import { OrderGrantRefundFormData } from "../form"; -import { grantRefundPageMessages } from "../messages"; -import { useRefundCardStyles } from "../styles"; - -interface RefundCardProps { - order: OrderDetailsGrantRefundFragment | null; - loading: boolean; - submitState: ConfirmButtonTransitionState; - isEdit: boolean; - submitDisabled: boolean; -} - -export const RefundCard = ({ - order, - loading, - submitState, - isEdit, - submitDisabled, -}: RefundCardProps) => { - const intl = useIntl(); - const { locale } = useLocale(); - const classes = useRefundCardStyles(); - const id = useId(); - - const { state, dispatch, form, totalSelectedPrice } = useGrantRefundContext(); - - const currency = order?.total?.gross?.currency ?? ""; - - return ( - - } - /> - - - - - {order ? ( -
- dispatch({ type: "toggleRefundShipping" })} - data-test-id="refundShippingCheckbox" - /> - -
- ) : ( -
- -
- )} - -
- - - - - {currency}{" "} - {formatMoneyAmount( - { - amount: totalSelectedPrice ?? 0, - currency, - }, - locale, - )} - - -
-
- -
-
- - {isEdit ? ( - - ) : ( - - )} - -
-
-
- ); -}; diff --git a/src/orders/components/OrderGrantRefundPage/components/ShippingInluded.tsx b/src/orders/components/OrderGrantRefundPage/components/ShippingInluded.tsx new file mode 100644 index 00000000000..58e058e761e --- /dev/null +++ b/src/orders/components/OrderGrantRefundPage/components/ShippingInluded.tsx @@ -0,0 +1,59 @@ +import { formatMoneyAmount } from "@dashboard/components/Money"; +import useLocale from "@dashboard/hooks/useLocale"; +import { IMoney } from "@dashboard/utils/intl"; +import { useId } from "@reach/auto-id"; +import { Box, Skeleton, Text, Toggle } from "@saleor/macaw-ui-next"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { useGrantRefundContext } from "../context"; +import { grantRefundPageMessages } from "../messages"; + +interface ShippingIncludedProps { + currency: string; + amount: IMoney; + canRefundShipping: boolean; +} + +export const ShippingIncluded = ({ + currency, + amount, + canRefundShipping, +}: ShippingIncludedProps) => { + const id = useId(); + const { locale } = useLocale(); + const { state, dispatch } = useGrantRefundContext(); + + return ( + + dispatch({ type: "toggleRefundShipping" })} + data-test-id="refundShippingCheckbox" + disabled={!currency || !canRefundShipping} + > + {!currency ? ( + + ) : ( + + )} + + + {!canRefundShipping && ( + + + + )} + + ); +}; diff --git a/src/orders/components/OrderGrantRefundPage/components/index.ts b/src/orders/components/OrderGrantRefundPage/components/index.ts deleted file mode 100644 index 10fb5c35502..00000000000 --- a/src/orders/components/OrderGrantRefundPage/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ProductCard"; -export * from "./RefundCard"; diff --git a/src/orders/components/OrderGrantRefundPage/form.ts b/src/orders/components/OrderGrantRefundPage/form.ts index cbb7fd5cb8e..b56e5d9844d 100644 --- a/src/orders/components/OrderGrantRefundPage/form.ts +++ b/src/orders/components/OrderGrantRefundPage/form.ts @@ -1,29 +1,48 @@ import { useExitFormDialog } from "@dashboard/components/Form/useExitFormDialog"; -import useForm from "@dashboard/hooks/useForm"; +import { OrderGrantRefundCreateLineInput } from "@dashboard/graphql"; +import useForm, { FormChange } from "@dashboard/hooks/useForm"; import useHandleFormSubmit from "@dashboard/hooks/useHandleFormSubmit"; import React from "react"; export interface OrderGrantRefundFormData { - amount: string; + amount: number | undefined; reason: string; + lines: OrderGrantRefundCreateLineInput[]; + grantRefundForShipping: boolean; } const defaultInitialData: OrderGrantRefundFormData = { - amount: "", + amount: 0, reason: "", + lines: [], + grantRefundForShipping: false, }; +export interface Line { + id: string; + quantity: number; +} + interface GrantRefundFormHookProps { onSubmit: (data: OrderGrantRefundFormData) => void; - initialData?: OrderGrantRefundFormData; + grantedRefund?: OrderGrantRefundFormData; + lines: Line[]; + grantRefundForShipping: boolean; } export const useGrantRefundForm = ({ onSubmit, - initialData, + grantedRefund, + lines, + grantRefundForShipping, }: GrantRefundFormHookProps) => { + const [isFormDirty, setIsFormDirty] = React.useState({ + amount: false, + reason: false, + }); + const { set, change, data, formId } = useForm( - initialData ?? defaultInitialData, + grantedRefund ?? defaultInitialData, undefined, { confirmLeave: true, @@ -39,9 +58,46 @@ export const useGrantRefundForm = ({ onSubmit, }); - const submit = () => handleFormSubmit(data); + const getAmountValue = () => { + // When editing always return the amount value + if (grantedRefund) { + return data.amount; + } + + // When creating and user doesn not provide value, value will be calculated base on lines and shipping + if (!isFormDirty.amount) { + return undefined; + } + + // When creating and user provide value, return the provided value + return data.amount; + }; + + const submit = () => + handleFormSubmit({ + ...data, + amount: getAmountValue(), + lines, + grantRefundForShipping, + }); React.useEffect(() => setExitDialogSubmitRef(submit), [submit]); - return { set, change, data, submit, setIsDirty }; + const handleChange: FormChange = e => { + if (e.target.name === "amount") + setIsFormDirty({ ...isFormDirty, amount: true }); + if (e.target.name === "reason") + setIsFormDirty({ ...isFormDirty, reason: true }); + + change(e); + }; + + return { + set, + change: handleChange, + data, + submit, + setIsDirty, + isFormDirty, + }; }; diff --git a/src/orders/components/OrderGrantRefundPage/reducer.ts b/src/orders/components/OrderGrantRefundPage/reducer.ts index 82bf283542b..bf2c54abcb4 100644 --- a/src/orders/components/OrderGrantRefundPage/reducer.ts +++ b/src/orders/components/OrderGrantRefundPage/reducer.ts @@ -1,11 +1,18 @@ // @ts-strict-ignore -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; +import { + OrderDetailsGrantedRefundFragment, + OrderDetailsGrantRefundFragment, +} from "@dashboard/graphql"; import { exhaustiveCheck } from "@dashboard/utils/ts"; +import { getLineAvailableQuantity } from "./utils"; + export interface ReducerOrderLine { selectedQuantity: number; availableQuantity: number; + initialQuantity?: number; unitPrice: number; + isDirty: boolean; } export interface GrantRefundState { @@ -20,10 +27,15 @@ export type GrantRefundAction = type: "setQuantity"; lineId: string; amount: number; + unitPrice: number; } | { type: "setMaxQuantity"; - lineIds: string[]; + lines: Array<{ + id: string; + quantity: number; + unitPrice: number; + }>; } | { type: "initState"; @@ -31,36 +43,29 @@ export type GrantRefundAction = } | { type: "toggleRefundShipping"; + } + | { + type: "setRefundShipping"; + refundShipping: boolean; }; export const getGrantRefundReducerInitialState = ( order: OrderDetailsGrantRefundFragment, + grantedRefund?: OrderDetailsGrantedRefundFragment, ): GrantRefundState => { + const toGrantRefundLine = createToGrantRefundLineMap(order, grantedRefund); + const unfulfilledLines = order?.lines .filter(line => line.quantityToFulfill > 0) - .map(line => [ - line.id, - { - availableQuantity: line.quantity, - unitPrice: line.unitPrice.gross.amount, - selectedQuantity: 0, - }, - ]); + .map(toGrantRefundLine); const fulfilmentLines = order.fulfillments .flatMap(fulfilment => fulfilment.lines) - .map(line => [ - line.id, - { - availableQuantity: line.quantity, - unitPrice: line.orderLine.unitPrice.gross.amount, - selectedQuantity: 0, - }, - ]); + .map(toGrantRefundLine); return { lines: new Map([...unfulfilledLines, ...fulfilmentLines]), - refundShipping: false, + refundShipping: grantedRefund?.shippingCostsIncluded ?? false, }; }; @@ -75,15 +80,13 @@ export function grantRefundReducer( ): GrantRefundState { switch (action.type) { case "setQuantity": { - if (!state.lines.has(action.lineId)) { - return state; - } - const line = state.lines.get(action.lineId); const newLines = new Map(state.lines); newLines.set(action.lineId, { ...line, + isDirty: action.amount !== line.initialQuantity, + unitPrice: action.unitPrice, selectedQuantity: action.amount, }); @@ -92,16 +95,16 @@ export function grantRefundReducer( lines: newLines, }; } - case "setMaxQuantity": { const newLines = new Map(state.lines); - action.lineIds.forEach(lineId => { - const line = state.lines.get(lineId); - - newLines.set(lineId, { - ...line, - selectedQuantity: line.availableQuantity, + action.lines.forEach(line => { + const currentLine = state.lines.get(line.id); + newLines.set(line.id, { + ...currentLine, + isDirty: line.quantity !== currentLine.initialQuantity, + unitPrice: line.unitPrice, + selectedQuantity: line.quantity, }); }); @@ -110,19 +113,57 @@ export function grantRefundReducer( lines: newLines, }; } - case "initState": { return action.state; } - case "toggleRefundShipping": { return { ...state, refundShipping: !state.refundShipping, }; } + case "setRefundShipping": { + return { + ...state, + refundShipping: action.refundShipping, + }; + } default: exhaustiveCheck(action); } } + +function createToGrantRefundLineMap( + order: OrderDetailsGrantRefundFragment, + grantedRefund?: OrderDetailsGrantedRefundFragment, +) { + return ( + line: + | OrderDetailsGrantRefundFragment["lines"][0] + | OrderDetailsGrantRefundFragment["fulfillments"][0]["lines"][0], + ): GrantRefundLineKeyValue => { + const initialQuantity = + grantedRefund?.lines?.find(initLine => initLine.orderLine.id === line.id) + ?.quantity ?? 0; + + return [ + line.id, + { + isDirty: false, + availableQuantity: getLineAvailableQuantity({ + lineId: line.id, + lineQuntity: line.quantity, + grantRefunds: order?.grantedRefunds, + grantRefundId: grantedRefund?.id, + }), + unitPrice: + "orderLine" in line + ? line.orderLine.unitPrice.gross.amount + : line.unitPrice.gross.amount, + selectedQuantity: initialQuantity, + initialQuantity, + }, + ]; + }; +} diff --git a/src/orders/components/OrderGrantRefundPage/styles.ts b/src/orders/components/OrderGrantRefundPage/styles.ts index 0359a4c831d..36e8e38f6cb 100644 --- a/src/orders/components/OrderGrantRefundPage/styles.ts +++ b/src/orders/components/OrderGrantRefundPage/styles.ts @@ -1,81 +1,11 @@ import { makeStyles } from "@saleor/macaw-ui"; -export const useStyles = makeStyles( - theme => ({ - fulfilmentNumber: { - display: "inline", - marginLeft: theme.spacing(1), - }, - cardsContainer: { - display: "flex", - flexDirection: "column", - gap: theme.spacing(2), - }, - cardLoading: { - height: "20em", - }, - form: { - display: "contents", - }, - }), - { name: "OrderGrantRefund" }, -); - -export const useRefundCardStyles = makeStyles( - theme => ({ - cardContent: { - display: "flex", - flexDirection: "column", - gap: theme.spacing(1.5), - }, - refundCardHeader: { - paddingBottom: theme.spacing(1), - }, - suggestedValue: { - display: "flex", - alignItems: "baseline", - gap: theme.spacing(1), - flexWrap: "wrap", - }, - totalMoney: { - fontWeight: 600, - }, - applyButton: { - height: "auto", - padding: 0, - }, - shippingCostLine: { - display: "flex", - gap: theme.spacing(1), - "& .MuiCheckbox-root": { - padding: 0, - }, - }, - submitLine: { - display: "flex", - "& button": { - // when line overflows - marginLeft: "auto", - }, - }, - shippingCostLineLoading: { - height: "21px", - }, - }), - { name: "RefundCard" }, -); - export const useProductsCardStyles = makeStyles( theme => { - const inputPadding = { - paddingBottom: theme.spacing(2), - paddingTop: theme.spacing(2), - }; return { colProduct: { width: "auto", }, - productVariantName: {}, productName: { display: "flex", flexDirection: "column", @@ -92,14 +22,6 @@ export const useProductsCardStyles = makeStyles( textAlign: "right", width: `${75 + 32 + 32}px`, }, - quantityInnerInput: { - ...inputPadding, - }, - remainingQuantity: { - ...inputPadding, - color: theme.palette.text.secondary, - whiteSpace: "nowrap", - }, }; }, { name: "ProductsCard" }, diff --git a/src/orders/components/OrderGrantRefundPage/utils.test.ts b/src/orders/components/OrderGrantRefundPage/utils.test.ts new file mode 100644 index 00000000000..faa0d70c79f --- /dev/null +++ b/src/orders/components/OrderGrantRefundPage/utils.test.ts @@ -0,0 +1,161 @@ +import { OrderDetailsGrantedRefundFragment } from "@dashboard/graphql"; + +import { + calculateCanRefundShipping, + getRefundAmountValue, + OrderGrantRefundData, +} from "./utils"; + +describe("OrderGrantRefundPage utils", () => { + describe("calculateCanRefundShipping", () => { + it("should return true is current edited granted refund has granted refund for shipping", () => { + // Arrange + const editedGrantedRefund = { + grantRefundForShipping: true, + grantRefundId: "1", + } as OrderGrantRefundData; + + const grantedRefunds = [ + { + id: "1", + shippingCostsIncluded: true, + }, + { + id: "2", + shippingCostsIncluded: false, + }, + { + id: "3", + shippingCostsIncluded: false, + }, + ] as OrderDetailsGrantedRefundFragment[]; + + // Act + const canRefundShipping = calculateCanRefundShipping( + editedGrantedRefund, + grantedRefunds, + ); + + // Assert + expect(canRefundShipping).toBe(true); + }); + + it("should return true is current edited granted refund does not have grantend shipping refund but no other granted refund has greanted shipping", () => { + // Arrange + const editedGrantedRefund = { + grantRefundForShipping: false, + grantRefundId: "1", + } as OrderGrantRefundData; + + const grantedRefunds = [ + { + id: "1", + shippingCostsIncluded: false, + }, + { + id: "2", + shippingCostsIncluded: false, + }, + { + id: "3", + shippingCostsIncluded: false, + }, + ] as OrderDetailsGrantedRefundFragment[]; + + // Act + const canRefundShipping = calculateCanRefundShipping( + editedGrantedRefund, + grantedRefunds, + ); + + // Assert + expect(canRefundShipping).toBe(true); + }); + + it("should return true when there is no current edited granted refund and no other granted refund has greanted shipping", () => { + // Arrange + const grantedRefunds = [ + { + id: "1", + shippingCostsIncluded: false, + }, + { + id: "2", + shippingCostsIncluded: false, + }, + { + id: "3", + shippingCostsIncluded: false, + }, + ] as OrderDetailsGrantedRefundFragment[]; + + // Act + const canRefundShipping = calculateCanRefundShipping( + undefined, + grantedRefunds, + ); + + // Assert + expect(canRefundShipping).toBe(true); + }); + }); + + describe("getRefundAmountValue", () => { + it("should return refund amount when user provided value and input is dirty", () => { + // Arrange + const isAmountInputDirty = true; + const isEditedRefundAmount = false; + const totalCalulatedPrice = 15; + const refundAmount = 10; + + // Act + const refundAmountValue = getRefundAmountValue({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, + }); + + // Assert + expect(refundAmountValue).toBe(refundAmount); + }); + + it("should return refund amount when user editing granted refund", () => { + // Arrange + const isAmountInputDirty = false; + const isEditedRefundAmount = true; + const totalCalulatedPrice = 15; + const refundAmount = 10; + + // Act + const refundAmountValue = getRefundAmountValue({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, + }); + + // Assert + expect(refundAmountValue).toBe(refundAmount); + }); + + it("should return total calculated when user create granted refund and change quantity or shipping ", () => { + // Arrange + const isAmountInputDirty = false; + const isEditedRefundAmount = false; + const totalCalulatedPrice = 25; + const refundAmount = 0; + + // Act + const refundAmountValue = getRefundAmountValue({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, + }); + + // Assert + expect(refundAmountValue).toBe(totalCalulatedPrice); + }); + }); +}); diff --git a/src/orders/components/OrderGrantRefundPage/utils.ts b/src/orders/components/OrderGrantRefundPage/utils.ts index 4602f3615e1..760c4e94992 100644 --- a/src/orders/components/OrderGrantRefundPage/utils.ts +++ b/src/orders/components/OrderGrantRefundPage/utils.ts @@ -1,7 +1,12 @@ -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; +import { + OrderDetailsGrantedRefundFragment, + OrderDetailsGrantRefundFragment, + OrderGrantRefundCreateLineInput, +} from "@dashboard/graphql"; import currency from "currency.js"; -import { GrantRefundState } from "./reducer"; +import { Line } from "./form"; +import { GrantRefundState, ReducerOrderLine } from "./reducer"; export const calculateTotalPrice = ( state: GrantRefundState, @@ -26,3 +31,99 @@ export const getFulfilmentSubtitle = ( order: OrderDetailsGrantRefundFragment, fulfillment: OrderDetailsGrantRefundFragment["fulfillments"][0], ) => `#${order.number}-${fulfillment.fulfillmentOrder}`; + +export const prepareLineData = (lines: Map): Line[] => + Array.from(lines.entries()) + .filter(([_, line]) => line.isDirty) + .map(([id, line]) => ({ + id, + quantity: line.selectedQuantity, + })); + +export const getLineAvailableQuantity = ({ + lineId, + lineQuntity, + grantRefunds, + grantRefundId, +}: { + lineId: string; + lineQuntity: number; + grantRefunds: OrderDetailsGrantRefundFragment["grantedRefunds"]; + grantRefundId?: string; +}) => { + let refundedQuantity = 0; + + grantRefunds.forEach(refund => { + if (grantRefundId && refund.id === grantRefundId) { + return; + } + + refund?.lines?.forEach(line => { + if (line.orderLine.id === lineId) { + refundedQuantity += line.quantity; + } + }); + }); + + return lineQuntity - refundedQuantity; +}; + +export interface OrderGrantRefundData { + amount: number; + reason: string; + lines: OrderGrantRefundCreateLineInput[]; + grantRefundForShipping: boolean; + grantRefundId: string; +} + +export const getGrantedRefundData = ( + grantedRefund?: OrderDetailsGrantedRefundFragment, +): OrderGrantRefundData | undefined => { + if (!grantedRefund) { + return undefined; + } + + return { + grantRefundId: grantedRefund.id, + reason: grantedRefund?.reason ?? "", + amount: grantedRefund.amount.amount, + grantRefundForShipping: grantedRefund.shippingCostsIncluded, + lines: grantedRefund?.lines ?? [], + }; +}; + +export const calculateCanRefundShipping = ( + editedGrantedRefund?: OrderGrantRefundData, + grantedRefunds?: OrderDetailsGrantedRefundFragment[], +) => { + if (editedGrantedRefund) { + if (editedGrantedRefund.grantRefundForShipping) { + return true; + } + return !grantedRefunds?.some( + refund => + refund.shippingCostsIncluded && + refund.id !== editedGrantedRefund.grantRefundId, + ); + } + return !grantedRefunds?.some(refund => refund.shippingCostsIncluded); +}; + +export const getRefundAmountValue = ({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, +}: { + isAmountInputDirty: boolean; + totalCalulatedPrice: number; + refundAmount: number; + isEditedRefundAmount: boolean; +}) => { + // User provided value into input or we are editing refund amount + if (isAmountInputDirty || isEditedRefundAmount) { + return refundAmount; + } + + return totalCalulatedPrice ?? 0; +}; diff --git a/src/orders/mutations.ts b/src/orders/mutations.ts index 4b682f312ae..81ab9dbdf50 100644 --- a/src/orders/mutations.ts +++ b/src/orders/mutations.ts @@ -510,12 +510,19 @@ export const orderTransactionRequestActionMutation = gql` export const orderGrantRefundAddMutation = gql` mutation OrderGrantRefundAdd( $orderId: ID! - $amount: Decimal! + $amount: Decimal $reason: String + $lines: [OrderGrantRefundCreateLineInput!] + $grantRefundForShipping: Boolean ) { orderGrantRefundCreate( id: $orderId - input: { amount: $amount, reason: $reason } + input: { + amount: $amount + reason: $reason + lines: $lines + grantRefundForShipping: $grantRefundForShipping + } ) { errors { ...OrderGrantRefundCreateError @@ -527,12 +534,21 @@ export const orderGrantRefundAddMutation = gql` export const orderGrantRefundEditMutation = gql` mutation OrderGrantRefundEdit( $refundId: ID! - $amount: Decimal! + $amount: Decimal $reason: String + $addLines: [OrderGrantRefundUpdateLineAddInput!] + $removeLines: [ID!] + $grantRefundForShipping: Boolean ) { orderGrantRefundUpdate( id: $refundId - input: { amount: $amount, reason: $reason } + input: { + amount: $amount + reason: $reason + addLines: $addLines + removeLines: $removeLines + grantRefundForShipping: $grantRefundForShipping + } ) { errors { ...OrderGrantRefundUpdateError diff --git a/src/orders/queries.ts b/src/orders/queries.ts index 4e975c10a31..4ec36cb6c0f 100644 --- a/src/orders/queries.ts +++ b/src/orders/queries.ts @@ -149,13 +149,6 @@ export const orderDetailsGrantedRefundEdit = gql` query OrderDetailsGrantRefundEdit($id: ID!) { order(id: $id) { ...OrderDetailsGrantRefund - grantedRefunds { - id - reason - amount { - ...Money - } - } } } `; diff --git a/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx b/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx index 305557056ff..e78a6b2af49 100644 --- a/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx +++ b/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx @@ -50,13 +50,45 @@ const OrderEditGrantRefund: React.FC = ({ }, }); - const handleSubmit = async ({ amount, reason }: OrderGrantRefundFormData) => { - extractMutationErrors( + const handleSubmit = async ({ + amount, + reason, + lines, + grantRefundForShipping, + }: OrderGrantRefundFormData) => { + const grantedRefundLinesToDelete = lines + .map(line => + grantedRefund.lines.find( + grandLine => grandLine.orderLine.id === line.id, + ), + ) + .filter(Boolean) + .map(line => line.id); + + if (grantedRefundLinesToDelete.length > 0) { + await extractMutationErrors( + grantRefund({ + variables: { + refundId: grantRefundId, + removeLines: grantedRefundLinesToDelete, + }, + }), + ); + } + + await extractMutationErrors( grantRefund({ variables: { refundId: grantRefundId, amount, reason, + grantRefundForShipping, + addLines: lines.map(line => ({ + id: line.id, + quantity: line.quantity, + reason: line.reason ?? "", + })), + removeLines: [], }, }), ); @@ -67,7 +99,7 @@ const OrderEditGrantRefund: React.FC = ({ undefined} isEdit /> @@ -93,10 +125,7 @@ const OrderEditGrantRefund: React.FC = ({ submitState={grantRefundOptions.status} onSubmit={handleSubmit} isEdit - initialData={{ - reason: grantedRefund.reason, - amount: grantedRefund.amount.amount.toString(), - }} + initialData={grantedRefund} /> ); diff --git a/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx b/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx index 3503af2751c..ac03c89e5d1 100644 --- a/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx +++ b/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx @@ -45,13 +45,26 @@ const OrderGrantRefund: React.FC = ({ orderId }) => { }, }); - const handleSubmit = async ({ amount, reason }: OrderGrantRefundFormData) => { + const handleSubmit = async ({ + amount, + reason, + lines, + grantRefundForShipping, + }: OrderGrantRefundFormData) => { + // API call should be stoped when use doesn't select any line, + // doesn't provide own amount and doesn't want to refund shipping + if (lines.length === 0 && amount === undefined && !grantRefundForShipping) { + return; + } + extractMutationErrors( grantRefund({ variables: { orderId, amount, reason, + lines, + grantRefundForShipping, }, }), );