From 232cd2a122f22f5a47b5485db96decbe16661b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:49:04 +0100 Subject: [PATCH] Select tax app on taxes configuration (#4631) --- .changeset/perfect-ears-drive.md | 5 ++ introspection.json | 84 ++++++++++++++++++ locale/defaultMessages.json | 22 +++++ playwright/pages/taxesPage.ts | 3 +- playwright/tests/taxes.spec.ts | 3 +- schema.graphql | 44 ++++++++++ .../AppAdditionalInfo.tsx} | 39 +++++---- .../components/AppAdditionalInfo/index.ts | 1 + .../messages.ts | 5 ++ src/apps/components/AppAvatar/AppAvatar.tsx | 9 +- .../AppDeleteDialog/AppDeleteDialog.tsx | 19 +++- .../components/AppDeleteDialog/messages.ts | 5 ++ .../components/AppListPage/AppListPage.tsx | 4 +- src/apps/components/AppListPage/utils.test.ts | 4 + src/apps/components/AppPermissions/index.ts | 2 - src/apps/components/AppPermissions/styles.ts | 11 --- .../InstalledAppListRow.tsx | 7 +- src/apps/fixtures.ts | 2 + src/fragments/apps.ts | 1 + src/fragments/taxes.ts | 2 + src/graphql/hooks.generated.ts | 48 ++++++++++ src/graphql/typePolicies.generated.ts | 9 +- src/graphql/types.generated.ts | 35 ++++++-- src/taxes/components/FlatTaxRateLabel.tsx | 18 ++++ src/taxes/components/LegacyFlowWarning.tsx | 35 ++++++++ src/taxes/components/PluginLabel.tsx | 18 ++++ src/taxes/components/TaxAppLabel.tsx | 87 +++++++++++++++++++ src/taxes/components/index.ts | 4 + src/taxes/fixtures.ts | 6 ++ .../pages/TaxChannelsPage/TaxChannelsPage.tsx | 57 +++++++----- .../TaxCountryExceptionListItem.tsx | 60 ++++++++----- .../TaxSettingsCard/TaxSettingsCard.tsx | 12 ++- .../TaxChannelsPage/TaxSettingsCard/styles.ts | 5 +- .../pages/TaxChannelsPage/helpers.test.ts | 39 +++++++++ src/taxes/pages/TaxChannelsPage/helpers.tsx | 25 ++++++ src/taxes/pages/TaxChannelsPage/styles.ts | 8 +- .../TaxChannelsPage/useTaxStrategyChoices.tsx | 44 ++++++++++ src/taxes/queries.ts | 19 ++++ 38 files changed, 701 insertions(+), 100 deletions(-) create mode 100644 .changeset/perfect-ears-drive.md rename src/apps/components/{AppPermissions/AppPermissions.tsx => AppAdditionalInfo/AppAdditionalInfo.tsx} (53%) create mode 100644 src/apps/components/AppAdditionalInfo/index.ts rename src/apps/components/{AppPermissions => AppAdditionalInfo}/messages.ts (74%) delete mode 100644 src/apps/components/AppPermissions/index.ts delete mode 100644 src/apps/components/AppPermissions/styles.ts create mode 100644 src/taxes/components/FlatTaxRateLabel.tsx create mode 100644 src/taxes/components/LegacyFlowWarning.tsx create mode 100644 src/taxes/components/PluginLabel.tsx create mode 100644 src/taxes/components/TaxAppLabel.tsx create mode 100644 src/taxes/components/index.ts create mode 100644 src/taxes/pages/TaxChannelsPage/helpers.test.ts create mode 100644 src/taxes/pages/TaxChannelsPage/helpers.tsx create mode 100644 src/taxes/pages/TaxChannelsPage/useTaxStrategyChoices.tsx diff --git a/.changeset/perfect-ears-drive.md b/.changeset/perfect-ears-drive.md new file mode 100644 index 00000000000..8df81b25728 --- /dev/null +++ b/.changeset/perfect-ears-drive.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Previously we allowed user to select either flat rates or any tax app. To avoid problems if there are more tax apps installed this change adds ability to select tax app that will be used to calculated taxes per channel. User can also select tax app for country exception while configuring taxes. Related [RFC](https://github.com/saleor/saleor/issues/12942) diff --git a/introspection.json b/introspection.json index 7bdf8e9e727..8fc68e388ba 100644 --- a/introspection.json +++ b/introspection.json @@ -5428,6 +5428,18 @@ "description": null, "fields": null, "inputFields": [ + { + "name": "identifier", + "description": "Canonical app ID. If not provided, the identifier will be generated based on app.id.\n\nAdded in Saleor 3.19.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "name", "description": "Name of the app.", @@ -116512,6 +116524,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "availableTaxApps", + "description": "List of tax apps that can be assigned to the channel. The list will be calculated by Saleor based on the apps that are subscribed to webhooks related to tax calculations: CHECKOUT_CALCULATE_TAXES\n\nAdded in Saleor 3.19.\n\nRequires one of the following permissions: AUTHENTICATED_STAFF_USER, MANAGE_APPS.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "App", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "channelCurrencies", "description": "List of all currencies supported by shop's channels.\n\nAdded in Saleor 3.1.\n\nRequires one of the following permissions: AUTHENTICATED_STAFF_USER, AUTHENTICATED_APP.", @@ -122411,6 +122447,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxAppId", + "description": "The tax app id that will be used to calculate the taxes for the given channel. Empty value for `TAX_APP` set as `taxCalculationStrategy` means that Saleor will iterate over all installed tax apps. If multiple tax apps exist with provided tax app id use the `App` with newest `created` date. Will become mandatory in 4.0 for `TAX_APP` `taxCalculationStrategy`.\n\nAdded in Saleor 3.19.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "taxCalculationStrategy", "description": "The default strategy to use for tax calculation in the given channel. Taxes can be calculated either using user-defined flat rates or with a tax app. Empty value means that no method is selected and taxes are not calculated.", @@ -122650,6 +122698,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxAppId", + "description": "The tax app id that will be used to calculate the taxes for the given channel and country. If not provided, use the value from the channel's tax configuration.\n\nAdded in Saleor 3.19.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "taxCalculationStrategy", "description": "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.", @@ -122722,6 +122782,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxAppId", + "description": "The tax app identifier that will be used to calculate the taxes for the given channel and country. If not provided, use the value from the channel's tax configuration.\n\nAdded in Saleor 3.19.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "taxCalculationStrategy", "description": "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.", @@ -122958,6 +123030,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxAppId", + "description": "The tax app id that will be used to calculate the taxes for the given channel. Empty value for `TAX_APP` set as `taxCalculationStrategy` means that Saleor will iterate over all installed tax apps. If multiple tax apps exist with provided tax app id use the `App` with newest `created` date. Will become mandatory in 4.0 for `TAX_APP` `taxCalculationStrategy`.\n\nAdded in Saleor 3.19.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "taxCalculationStrategy", "description": "The default strategy to use for tax calculation in the given channel. Taxes can be calculated either using user-defined flat rates or with a tax app. Empty value means that no method is selected and taxes are not calculated.", diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 0082f778f61..f919e44ec2e 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -59,6 +59,9 @@ "context": "tabel column header", "string": "Total" }, + "+Ps1jL": { + "string": "Be extra careful with taxes and payment apps, ensure your configuration selects other apps to be used" + }, "+RffqY": { "context": "Dry run dialog object title", "string": "Select object type to perform dry run on provided query" @@ -4982,6 +4985,9 @@ "context": "dialog title", "string": "Add Address" }, + "W2OIhn": { + "string": "Use app: {name}" + }, "W32xfN": { "context": "staff member full name", "string": "Name" @@ -5198,6 +5204,9 @@ "context": "discount type", "string": "Fixed amount" }, + "XFKV5Z": { + "string": "Created at: {date}" + }, "XFtKV5": { "context": "input placeholder tag", "string": "Tag" @@ -6704,6 +6713,9 @@ "context": "button", "string": "Send invite" }, + "hyAOPB": { + "string": "Use Avalara plugin" + }, "hz+9ES": { "context": "bulk activate label", "string": "Activate" @@ -6979,6 +6991,9 @@ "k0rGBI": { "string": "Access token is used to authenticate service accounts" }, + "k20lqw": { + "string": "Legacy flow detected - select tax strategy from dropdown" + }, "k3EI/U": { "context": "dialog header", "string": "Delete Shipping Zone" @@ -8788,6 +8803,10 @@ "wQdR8M": { "string": "Add search engine title and description to make this category easier to find" }, + "wRvjoc": { + "context": "app created at label", + "string": "Created at" + }, "wTHjt3": { "string": "Search Orders..." }, @@ -9187,6 +9206,9 @@ "context": "order history message", "string": "Order was cancelled" }, + "zSDfq0": { + "string": "Use flat tax rate" + }, "zSOvI0": { "string": "Filters" }, diff --git a/playwright/pages/taxesPage.ts b/playwright/pages/taxesPage.ts index 90cc779c54a..53840ce39b3 100644 --- a/playwright/pages/taxesPage.ts +++ b/playwright/pages/taxesPage.ts @@ -125,8 +125,7 @@ export class TaxesPage extends BasePage { async clickCreateClassButton() { await this.createClassButton.click(); } - - async selectTaxCalculationMethod(method: "FLAT_RATES" | "TAX_APP") { + async selectTaxCalculationMethod(method: "FLAT_RATES" | "saleor.app.avatax") { await this.clickSelectMethodField(); await this.page.getByTestId(`select-field-option-${method}`).click(); } diff --git a/playwright/tests/taxes.spec.ts b/playwright/tests/taxes.spec.ts index f3c6f2f90dd..3c5ada7736b 100644 --- a/playwright/tests/taxes.spec.ts +++ b/playwright/tests/taxes.spec.ts @@ -17,10 +17,11 @@ test("TC: SALEOR_115 Change taxes in channel to use tax app @taxes @e2e", async await configurationPage.gotoConfigurationView(); await configurationPage.openTaxes(); await taxesPage.selectChannel(CHANNELS.channelForTaxEdition.name); - await taxesPage.selectTaxCalculationMethod("TAX_APP"); + await taxesPage.selectTaxCalculationMethod("saleor.app.avatax"); await taxesPage.clickSaveButton(); await taxesPage.expectSuccessBanner(); }); + test("TC: SALEOR_116 Change taxes in channel: enter prices without tax, do not show gross price, add country exception @taxes @e2e", async () => { await taxesPage.gotoChannelsTabUrl(); await taxesPage.selectChannel(CHANNELS.channelForTaxEdition.name); diff --git a/schema.graphql b/schema.graphql index 2b59d98e615..b317e6325cf 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1323,6 +1323,13 @@ input AppFilterInput { } input AppInput { + """ + Canonical app ID. If not provided, the identifier will be generated based on app.id. + + Added in Saleor 3.19. + """ + identifier: String + """Name of the app.""" name: String @@ -29732,6 +29739,15 @@ type Shop implements ObjectWithMetadata { channel: String! ): [ShippingMethod!] + """ + List of tax apps that can be assigned to the channel. The list will be calculated by Saleor based on the apps that are subscribed to webhooks related to tax calculations: CHECKOUT_CALCULATE_TAXES + + Added in Saleor 3.19. + + Requires one of the following permissions: AUTHENTICATED_STAFF_USER, MANAGE_APPS. + """ + availableTaxApps: [App!]! + """ List of all currencies supported by shop's channels. @@ -31208,6 +31224,13 @@ type TaxConfiguration implements Node & ObjectWithMetadata { """ privateMetafields(keys: [String!]): Metadata + """ + The tax app id that will be used to calculate the taxes for the given channel. Empty value for `TAX_APP` set as `taxCalculationStrategy` means that Saleor will iterate over all installed tax apps. If multiple tax apps exist with provided tax app id use the `App` with newest `created` date. Will become mandatory in 4.0 for `TAX_APP` `taxCalculationStrategy`. + + Added in Saleor 3.19. + """ + taxAppId: String + """ The default strategy to use for tax calculation in the given channel. Taxes can be calculated either using user-defined flat rates or with a tax app. Empty value means that no method is selected and taxes are not calculated. """ @@ -31254,6 +31277,13 @@ type TaxConfigurationPerCountry { """ displayGrossPrices: Boolean! + """ + The tax app id that will be used to calculate the taxes for the given channel and country. If not provided, use the value from the channel's tax configuration. + + Added in Saleor 3.19. + """ + taxAppId: String + """ 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. """ @@ -31272,6 +31302,13 @@ input TaxConfigurationPerCountryInput { """ displayGrossPrices: Boolean! + """ + The tax app identifier that will be used to calculate the taxes for the given channel and country. If not provided, use the value from the channel's tax configuration. + + Added in Saleor 3.19. + """ + taxAppId: String + """ 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. """ @@ -31327,6 +31364,13 @@ input TaxConfigurationUpdateInput { """List of country codes for which to remove the tax configuration.""" removeCountriesConfiguration: [CountryCode!] + """ + The tax app id that will be used to calculate the taxes for the given channel. Empty value for `TAX_APP` set as `taxCalculationStrategy` means that Saleor will iterate over all installed tax apps. If multiple tax apps exist with provided tax app id use the `App` with newest `created` date. Will become mandatory in 4.0 for `TAX_APP` `taxCalculationStrategy`. + + Added in Saleor 3.19. + """ + taxAppId: String + """ The default strategy to use for tax calculation in the given channel. Taxes can be calculated either using user-defined flat rates or with a tax app. Empty value means that no method is selected and taxes are not calculated. """ diff --git a/src/apps/components/AppPermissions/AppPermissions.tsx b/src/apps/components/AppAdditionalInfo/AppAdditionalInfo.tsx similarity index 53% rename from src/apps/components/AppPermissions/AppPermissions.tsx rename to src/apps/components/AppAdditionalInfo/AppAdditionalInfo.tsx index 959ace8d24d..fd98847432c 100644 --- a/src/apps/components/AppPermissions/AppPermissions.tsx +++ b/src/apps/components/AppAdditionalInfo/AppAdditionalInfo.tsx @@ -1,20 +1,15 @@ import { AppPermissionFragment } from "@dashboard/graphql"; -import { Box, InfoIcon, Tooltip } from "@saleor/macaw-ui-next"; +import { Box, InfoIcon, Text, Tooltip } from "@saleor/macaw-ui-next"; +import moment from "moment"; import React from "react"; import { FormattedMessage } from "react-intl"; import { messages } from "./messages"; -import { useStyles } from "./styles"; -interface AppPermissionsProps { +export const AppAdditionalInfo: React.FC<{ permissions?: AppPermissionFragment[] | null; -} - -export const AppPermissions: React.FC = ({ - permissions, -}) => { - const classes = useStyles(); - + created: string | null; +}> = ({ permissions, created }) => { return ( @@ -27,20 +22,30 @@ export const AppPermissions: React.FC = ({ -
    + {permissions?.length ? ( permissions?.map(permission => ( -
  • {permission.name}
  • + + {permission.name} + )) ) : ( -
  • + -
  • +
    )} -
+ + {created && ( + <> + + + + + {moment(created).format("YYYY-MM-DD HH:mm")} + + + )}
); }; -AppPermissions.displayName = "AppPermissions"; -export default AppPermissions; diff --git a/src/apps/components/AppAdditionalInfo/index.ts b/src/apps/components/AppAdditionalInfo/index.ts new file mode 100644 index 00000000000..3308db13936 --- /dev/null +++ b/src/apps/components/AppAdditionalInfo/index.ts @@ -0,0 +1 @@ +export { AppAdditionalInfo } from "./AppAdditionalInfo"; diff --git a/src/apps/components/AppPermissions/messages.ts b/src/apps/components/AppAdditionalInfo/messages.ts similarity index 74% rename from src/apps/components/AppPermissions/messages.ts rename to src/apps/components/AppAdditionalInfo/messages.ts index 7cef4df38d9..72f97daf7e0 100644 --- a/src/apps/components/AppPermissions/messages.ts +++ b/src/apps/components/AppAdditionalInfo/messages.ts @@ -11,4 +11,9 @@ export const messages = defineMessages({ defaultMessage: "None", description: "app permissions label", }, + createdAt: { + id: "wRvjoc", + defaultMessage: "Created at", + description: "app created at label", + }, }); diff --git a/src/apps/components/AppAvatar/AppAvatar.tsx b/src/apps/components/AppAvatar/AppAvatar.tsx index 52920884850..fad9c8e19a5 100644 --- a/src/apps/components/AppAvatar/AppAvatar.tsx +++ b/src/apps/components/AppAvatar/AppAvatar.tsx @@ -1,8 +1,10 @@ import { AppLogo } from "@dashboard/apps/types"; import { Box, GenericAppIcon } from "@saleor/macaw-ui-next"; import React from "react"; + type Logo = AppLogo | undefined; type Size = 8 | 12; + export const AppAvatar: React.FC<{ logo?: Logo; size?: Size; @@ -15,11 +17,10 @@ export const AppAvatar: React.FC<{ placeItems="center" borderRadius={2} > - + ) : ( - + ); diff --git a/src/apps/components/AppDeleteDialog/AppDeleteDialog.tsx b/src/apps/components/AppDeleteDialog/AppDeleteDialog.tsx index e63e6d280b5..d22bc017629 100644 --- a/src/apps/components/AppDeleteDialog/AppDeleteDialog.tsx +++ b/src/apps/components/AppDeleteDialog/AppDeleteDialog.tsx @@ -2,6 +2,7 @@ import ActionDialog from "@dashboard/components/ActionDialog"; import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { getStringOrPlaceholder } from "@dashboard/misc"; import { DialogContentText } from "@material-ui/core"; +import { Box, Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -55,7 +56,23 @@ const AppDeleteDialog: React.FC = ({ title={intl.formatMessage(msgs.deleteAppTitle)} variant="delete" > - + + + + {intl.formatMessage(msgs.deleteAppWarning)} + + {getMainText()} diff --git a/src/apps/components/AppDeleteDialog/messages.ts b/src/apps/components/AppDeleteDialog/messages.ts index 885b0141dc4..855d1d22c3e 100644 --- a/src/apps/components/AppDeleteDialog/messages.ts +++ b/src/apps/components/AppDeleteDialog/messages.ts @@ -35,4 +35,9 @@ export default defineMessages({ defaultMessage: "Are you sure you want to delete this app?", description: "delete app", }, + deleteAppWarning: { + id: "+Ps1jL", + defaultMessage: + "Be extra careful with taxes and payment apps, ensure your configuration selects other apps to be used", + }, }); diff --git a/src/apps/components/AppListPage/AppListPage.tsx b/src/apps/components/AppListPage/AppListPage.tsx index db3b8e59543..92751616753 100644 --- a/src/apps/components/AppListPage/AppListPage.tsx +++ b/src/apps/components/AppListPage/AppListPage.tsx @@ -42,7 +42,7 @@ export const AppListPage: React.FC = props => { installedApps, installableMarketplaceApps, ); - const verifiedAppsIntallations = getVerifiedAppsInstallations( + const verifiedAppsInstallations = getVerifiedAppsInstallations( appsInstallations, installableMarketplaceApps, ); @@ -104,7 +104,7 @@ export const AppListPage: React.FC = props => { { type: AppTypeEnum.THIRDPARTY, version: "1.0.0", appUrl: null, + created: "2020-06-02T12:24:26.818138+00:00", manifestUrl: "https://www.example.com/manifest", permissions: [ { @@ -197,6 +198,7 @@ describe("App List verified installed apps util", () => { type: AppTypeEnum.THIRDPARTY, version: "1.0.0", appUrl: "http://localhost:3000", + created: "2020-06-02T12:24:26.818138+00:00", manifestUrl: "http://localhost:3000/api/manifest", permissions: [ { @@ -269,6 +271,7 @@ describe("App List verified installable marketplace apps util", () => { version: "1.0.0", appUrl: null, manifestUrl: "https://www.example.com/manifest", + created: "2020-06-02T12:24:26.818138+00:00", permissions: [ { __typename: "Permission", @@ -287,6 +290,7 @@ describe("App List verified installable marketplace apps util", () => { version: "1.0.0", appUrl: "http://localhost:3000", manifestUrl: "http://localhost:3000/api/manifest", + created: "2020-06-02T12:24:26.818138+00:00", permissions: [ { __typename: "Permission", diff --git a/src/apps/components/AppPermissions/index.ts b/src/apps/components/AppPermissions/index.ts deleted file mode 100644 index 37c02d998fd..00000000000 --- a/src/apps/components/AppPermissions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AppPermissions } from "./AppPermissions"; -export { default } from "./AppPermissions"; diff --git a/src/apps/components/AppPermissions/styles.ts b/src/apps/components/AppPermissions/styles.ts deleted file mode 100644 index a160ebb2521..00000000000 --- a/src/apps/components/AppPermissions/styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { makeStyles } from "@saleor/macaw-ui"; - -export const useStyles = makeStyles( - () => ({ - list: { - margin: 0, - paddingLeft: "16px", - }, - }), - { name: "AppPermissions" }, -); diff --git a/src/apps/components/InstalledAppListRow/InstalledAppListRow.tsx b/src/apps/components/InstalledAppListRow/InstalledAppListRow.tsx index 6f193773e20..1a6989e7004 100644 --- a/src/apps/components/InstalledAppListRow/InstalledAppListRow.tsx +++ b/src/apps/components/InstalledAppListRow/InstalledAppListRow.tsx @@ -8,8 +8,8 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useLocation } from "react-router"; +import { AppAdditionalInfo } from "../AppAdditionalInfo"; import { AppAvatar } from "../AppAvatar/AppAvatar"; -import AppPermissions from "../AppPermissions"; import { AppManifestUrl } from "./AppManifestUrl"; import { messages } from "./messages"; @@ -105,7 +105,10 @@ export const InstalledAppListRow: React.FC = props => { )} - + diff --git a/src/apps/fixtures.ts b/src/apps/fixtures.ts index 60ed509ece2..1b9e515dd5d 100644 --- a/src/apps/fixtures.ts +++ b/src/apps/fixtures.ts @@ -20,6 +20,7 @@ export const activeApp: AppListItemFragment = { version: "1.0.0", appUrl: "http://localhost:3000", manifestUrl: "http://localhost:3000/api/manifest", + created: "2020-06-02T12:24:26.818138+00:00", permissions: [ { __typename: "Permission", @@ -39,6 +40,7 @@ export const inactiveApp: AppListItemFragment = { version: "1.0.0", appUrl: null, manifestUrl: "http://localhost:3000/api/manifest", + created: "2020-06-02T12:24:26.818138+00:00", permissions: [ { __typename: "Permission", diff --git a/src/fragments/apps.ts b/src/fragments/apps.ts index b7c893eee82..da2a4616c41 100644 --- a/src/fragments/apps.ts +++ b/src/fragments/apps.ts @@ -87,6 +87,7 @@ export const appListItemFragment = gql` appUrl manifestUrl version + created brand { logo { default(format: WEBP, size: 64) diff --git a/src/fragments/taxes.ts b/src/fragments/taxes.ts index 8477ef03e91..b595cce3fd3 100644 --- a/src/fragments/taxes.ts +++ b/src/fragments/taxes.ts @@ -24,6 +24,7 @@ export const taxConfigurationPerCountry = gql` } chargeTaxes taxCalculationStrategy + taxAppId displayGrossPrices } `; @@ -39,6 +40,7 @@ export const taxConfiguration = gql` pricesEnteredWithTax chargeTaxes taxCalculationStrategy + taxAppId countries { ...TaxConfigurationPerCountry } diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 857de1c5c1f..112cc8c043d 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -106,6 +106,7 @@ export const AppListItemFragmentDoc = gql` appUrl manifestUrl version + created brand { logo { default(format: WEBP, size: 64) @@ -2974,6 +2975,7 @@ export const TaxConfigurationPerCountryFragmentDoc = gql` } chargeTaxes taxCalculationStrategy + taxAppId displayGrossPrices } ${CountryWithCodeFragmentDoc}`; @@ -2988,6 +2990,7 @@ export const TaxConfigurationFragmentDoc = gql` pricesEnteredWithTax chargeTaxes taxCalculationStrategy + taxAppId countries { ...TaxConfigurationPerCountry } @@ -18073,6 +18076,51 @@ export function useTaxClassAssignLazyQuery(baseOptions?: ApolloReactHooks.LazyQu export type TaxClassAssignQueryHookResult = ReturnType; export type TaxClassAssignLazyQueryHookResult = ReturnType; export type TaxClassAssignQueryResult = Apollo.QueryResult; +export const TaxStrategyChoicesDocument = gql` + query TaxStrategyChoices { + shop { + availableTaxApps { + id + name + version + identifier + created + brand { + logo { + default + } + } + } + } +} + `; + +/** + * __useTaxStrategyChoicesQuery__ + * + * To run a query within a React component, call `useTaxStrategyChoicesQuery` and pass it any options that fit your needs. + * When your component renders, `useTaxStrategyChoicesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useTaxStrategyChoicesQuery({ + * variables: { + * }, + * }); + */ +export function useTaxStrategyChoicesQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(TaxStrategyChoicesDocument, options); + } +export function useTaxStrategyChoicesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(TaxStrategyChoicesDocument, options); + } +export type TaxStrategyChoicesQueryHookResult = ReturnType; +export type TaxStrategyChoicesLazyQueryHookResult = ReturnType; +export type TaxStrategyChoicesQueryResult = Apollo.QueryResult; export const UpdateProductTranslationsDocument = gql` mutation UpdateProductTranslations($id: ID!, $input: TranslationInput!, $language: LanguageCodeEnum!) { productTranslate(id: $id, input: $input, languageCode: $language) { diff --git a/src/graphql/typePolicies.generated.ts b/src/graphql/typePolicies.generated.ts index 96a6c3b8b65..8667a9c213b 100644 --- a/src/graphql/typePolicies.generated.ts +++ b/src/graphql/typePolicies.generated.ts @@ -5638,13 +5638,14 @@ export type ShippingZoneUpdatedFieldPolicy = { shippingZone?: FieldPolicy | FieldReadFunction, version?: FieldPolicy | FieldReadFunction }; -export type ShopKeySpecifier = ('allowLoginWithoutConfirmation' | 'automaticFulfillmentDigitalProducts' | 'availableExternalAuthentications' | 'availablePaymentGateways' | 'availableShippingMethods' | 'channelCurrencies' | 'chargeTaxesOnShipping' | 'companyAddress' | 'countries' | 'customerSetPasswordUrl' | 'defaultCountry' | 'defaultDigitalMaxDownloads' | 'defaultDigitalUrlValidDays' | 'defaultMailSenderAddress' | 'defaultMailSenderName' | 'defaultWeightUnit' | 'description' | 'displayGrossPrices' | 'domain' | 'enableAccountConfirmationByEmail' | 'fulfillmentAllowUnpaid' | 'fulfillmentAutoApprove' | 'headerText' | 'id' | 'includeTaxesInPrices' | 'languages' | 'limitQuantityPerCheckout' | 'limits' | 'metadata' | 'metafield' | 'metafields' | 'name' | 'permissions' | 'phonePrefixes' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'reserveStockDurationAnonymousUser' | 'reserveStockDurationAuthenticatedUser' | 'schemaVersion' | 'staffNotificationRecipients' | 'trackInventoryByDefault' | 'translation' | 'version' | ShopKeySpecifier)[]; +export type ShopKeySpecifier = ('allowLoginWithoutConfirmation' | 'automaticFulfillmentDigitalProducts' | 'availableExternalAuthentications' | 'availablePaymentGateways' | 'availableShippingMethods' | 'availableTaxApps' | 'channelCurrencies' | 'chargeTaxesOnShipping' | 'companyAddress' | 'countries' | 'customerSetPasswordUrl' | 'defaultCountry' | 'defaultDigitalMaxDownloads' | 'defaultDigitalUrlValidDays' | 'defaultMailSenderAddress' | 'defaultMailSenderName' | 'defaultWeightUnit' | 'description' | 'displayGrossPrices' | 'domain' | 'enableAccountConfirmationByEmail' | 'fulfillmentAllowUnpaid' | 'fulfillmentAutoApprove' | 'headerText' | 'id' | 'includeTaxesInPrices' | 'languages' | 'limitQuantityPerCheckout' | 'limits' | 'metadata' | 'metafield' | 'metafields' | 'name' | 'permissions' | 'phonePrefixes' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'reserveStockDurationAnonymousUser' | 'reserveStockDurationAuthenticatedUser' | 'schemaVersion' | 'staffNotificationRecipients' | 'trackInventoryByDefault' | 'translation' | 'version' | ShopKeySpecifier)[]; export type ShopFieldPolicy = { allowLoginWithoutConfirmation?: FieldPolicy | FieldReadFunction, automaticFulfillmentDigitalProducts?: FieldPolicy | FieldReadFunction, availableExternalAuthentications?: FieldPolicy | FieldReadFunction, availablePaymentGateways?: FieldPolicy | FieldReadFunction, availableShippingMethods?: FieldPolicy | FieldReadFunction, + availableTaxApps?: FieldPolicy | FieldReadFunction, channelCurrencies?: FieldPolicy | FieldReadFunction, chargeTaxesOnShipping?: FieldPolicy | FieldReadFunction, companyAddress?: FieldPolicy | FieldReadFunction, @@ -5972,7 +5973,7 @@ export type TaxClassUpdateErrorFieldPolicy = { field?: FieldPolicy | FieldReadFunction, message?: FieldPolicy | FieldReadFunction }; -export type TaxConfigurationKeySpecifier = ('channel' | 'chargeTaxes' | 'countries' | 'displayGrossPrices' | 'id' | 'metadata' | 'metafield' | 'metafields' | 'pricesEnteredWithTax' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'taxCalculationStrategy' | TaxConfigurationKeySpecifier)[]; +export type TaxConfigurationKeySpecifier = ('channel' | 'chargeTaxes' | 'countries' | 'displayGrossPrices' | 'id' | 'metadata' | 'metafield' | 'metafields' | 'pricesEnteredWithTax' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'taxAppId' | 'taxCalculationStrategy' | TaxConfigurationKeySpecifier)[]; export type TaxConfigurationFieldPolicy = { channel?: FieldPolicy | FieldReadFunction, chargeTaxes?: FieldPolicy | FieldReadFunction, @@ -5986,6 +5987,7 @@ export type TaxConfigurationFieldPolicy = { privateMetadata?: FieldPolicy | FieldReadFunction, privateMetafield?: FieldPolicy | FieldReadFunction, privateMetafields?: FieldPolicy | FieldReadFunction, + taxAppId?: FieldPolicy | FieldReadFunction, taxCalculationStrategy?: FieldPolicy | FieldReadFunction }; export type TaxConfigurationCountableConnectionKeySpecifier = ('edges' | 'pageInfo' | 'totalCount' | TaxConfigurationCountableConnectionKeySpecifier)[]; @@ -5999,11 +6001,12 @@ export type TaxConfigurationCountableEdgeFieldPolicy = { cursor?: FieldPolicy | FieldReadFunction, node?: FieldPolicy | FieldReadFunction }; -export type TaxConfigurationPerCountryKeySpecifier = ('chargeTaxes' | 'country' | 'displayGrossPrices' | 'taxCalculationStrategy' | TaxConfigurationPerCountryKeySpecifier)[]; +export type TaxConfigurationPerCountryKeySpecifier = ('chargeTaxes' | 'country' | 'displayGrossPrices' | 'taxAppId' | 'taxCalculationStrategy' | TaxConfigurationPerCountryKeySpecifier)[]; export type TaxConfigurationPerCountryFieldPolicy = { chargeTaxes?: FieldPolicy | FieldReadFunction, country?: FieldPolicy | FieldReadFunction, displayGrossPrices?: FieldPolicy | FieldReadFunction, + taxAppId?: FieldPolicy | FieldReadFunction, taxCalculationStrategy?: FieldPolicy | FieldReadFunction }; export type TaxConfigurationUpdateKeySpecifier = ('errors' | 'taxConfiguration' | TaxConfigurationUpdateKeySpecifier)[]; diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 9fd04c2ba69..d358a8e635c 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -222,6 +222,12 @@ export type AppFilterInput = { }; export type AppInput = { + /** + * Canonical app ID. If not provided, the identifier will be generated based on app.id. + * + * Added in Saleor 3.19. + */ + identifier?: InputMaybe; /** Name of the app. */ name?: InputMaybe; /** List of permission code names to assign to this app. */ @@ -6883,6 +6889,12 @@ export type TaxConfigurationPerCountryInput = { countryCode: CountryCode; /** Determines whether displayed prices should include taxes for this country. */ displayGrossPrices: Scalars['Boolean']; + /** + * The tax app identifier that will be used to calculate the taxes for the given channel and country. If not provided, use the value from the channel's tax configuration. + * + * Added in Saleor 3.19. + */ + taxAppId?: InputMaybe; /** 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; }; @@ -6904,6 +6916,12 @@ export type TaxConfigurationUpdateInput = { pricesEnteredWithTax?: InputMaybe; /** List of country codes for which to remove the tax configuration. */ removeCountriesConfiguration?: InputMaybe>; + /** + * The tax app id that will be used to calculate the taxes for the given channel. Empty value for `TAX_APP` set as `taxCalculationStrategy` means that Saleor will iterate over all installed tax apps. If multiple tax apps exist with provided tax app id use the `App` with newest `created` date. Will become mandatory in 4.0 for `TAX_APP` `taxCalculationStrategy`. + * + * Added in Saleor 3.19. + */ + taxAppId?: InputMaybe; /** The default strategy to use for tax calculation in the given channel. Taxes can be calculated either using user-defined flat rates or with a tax app. Empty value means that no method is selected and taxes are not calculated. */ taxCalculationStrategy?: InputMaybe; /** List of tax country configurations to create or update (identified by a country code). */ @@ -8937,7 +8955,7 @@ export type AppsListQueryVariables = Exact<{ }>; -export type AppsListQuery = { __typename: 'Query', apps: { __typename: 'AppCountableConnection', totalCount: number | null, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ __typename: 'AppCountableEdge', node: { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, brand: { __typename: 'AppBrand', logo: { __typename: 'AppBrandLogo', default: string } } | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null } }> } | null }; +export type AppsListQuery = { __typename: 'Query', apps: { __typename: 'AppCountableConnection', totalCount: number | null, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ __typename: 'AppCountableEdge', node: { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, created: any | null, brand: { __typename: 'AppBrand', logo: { __typename: 'AppBrandLogo', default: string } } | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null } }> } | null }; export type AppsInstallationsQueryVariables = Exact<{ [key: string]: never; }>; @@ -9873,7 +9891,7 @@ export type AppFragment = { __typename: 'App', id: string, name: string | null, export type AppInstallationFragment = { __typename: 'AppInstallation', status: JobStatusEnum, message: string | null, appName: string, manifestUrl: string, id: string, brand: { __typename: 'AppBrand', logo: { __typename: 'AppBrandLogo', default: string } } | null }; -export type AppListItemFragment = { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, brand: { __typename: 'AppBrand', logo: { __typename: 'AppBrandLogo', default: string } } | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null }; +export type AppListItemFragment = { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, created: any | null, brand: { __typename: 'AppBrand', logo: { __typename: 'AppBrandLogo', default: string } } | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null }; export type AppPermissionFragment = { __typename: 'Permission', name: string, code: PermissionEnum }; @@ -10331,9 +10349,9 @@ export type TaxedMoneyFragment = { __typename: 'TaxedMoney', net: { __typename: export type CountryFragment = { __typename: 'CountryDisplay', country: string, code: string }; -export type TaxConfigurationPerCountryFragment = { __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }; +export type TaxConfigurationPerCountryFragment = { __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }; -export type TaxConfigurationFragment = { __typename: 'TaxConfiguration', id: string, displayGrossPrices: boolean, pricesEnteredWithTax: boolean, chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, channel: { __typename: 'Channel', id: string, name: string }, countries: Array<{ __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }> }; +export type TaxConfigurationFragment = { __typename: 'TaxConfiguration', id: string, displayGrossPrices: boolean, pricesEnteredWithTax: boolean, chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, channel: { __typename: 'Channel', id: string, name: string }, countries: Array<{ __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }> }; export type TaxCountryConfigurationFragment = { __typename: 'TaxCountryConfiguration', country: { __typename: 'CountryDisplay', country: string, code: string }, taxClassCountryRates: Array<{ __typename: 'TaxClassCountryRate', rate: number, taxClass: { __typename: 'TaxClass', id: string, name: string } | null }> }; @@ -12091,7 +12109,7 @@ export type TaxConfigurationUpdateMutationVariables = Exact<{ }>; -export type TaxConfigurationUpdateMutation = { __typename: 'Mutation', taxConfigurationUpdate: { __typename: 'TaxConfigurationUpdate', errors: Array<{ __typename: 'TaxConfigurationUpdateError', field: string | null, code: TaxConfigurationUpdateErrorCode, message: string | null }>, taxConfiguration: { __typename: 'TaxConfiguration', id: string, displayGrossPrices: boolean, pricesEnteredWithTax: boolean, chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, channel: { __typename: 'Channel', id: string, name: string }, countries: Array<{ __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }> } | null } | null }; +export type TaxConfigurationUpdateMutation = { __typename: 'Mutation', taxConfigurationUpdate: { __typename: 'TaxConfigurationUpdate', errors: Array<{ __typename: 'TaxConfigurationUpdateError', field: string | null, code: TaxConfigurationUpdateErrorCode, message: string | null }>, taxConfiguration: { __typename: 'TaxConfiguration', id: string, displayGrossPrices: boolean, pricesEnteredWithTax: boolean, chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, channel: { __typename: 'Channel', id: string, name: string }, countries: Array<{ __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }> } | null } | null }; export type TaxCountryConfigurationUpdateMutationVariables = Exact<{ countryCode: CountryCode; @@ -12139,7 +12157,7 @@ export type TaxConfigurationsListQueryVariables = Exact<{ }>; -export type TaxConfigurationsListQuery = { __typename: 'Query', taxConfigurations: { __typename: 'TaxConfigurationCountableConnection', edges: Array<{ __typename: 'TaxConfigurationCountableEdge', node: { __typename: 'TaxConfiguration', id: string, displayGrossPrices: boolean, pricesEnteredWithTax: boolean, chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, channel: { __typename: 'Channel', id: string, name: string }, countries: Array<{ __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }> } }> } | null }; +export type TaxConfigurationsListQuery = { __typename: 'Query', taxConfigurations: { __typename: 'TaxConfigurationCountableConnection', edges: Array<{ __typename: 'TaxConfigurationCountableEdge', node: { __typename: 'TaxConfiguration', id: string, displayGrossPrices: boolean, pricesEnteredWithTax: boolean, chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, channel: { __typename: 'Channel', id: string, name: string }, countries: Array<{ __typename: 'TaxConfigurationPerCountry', chargeTaxes: boolean, taxCalculationStrategy: TaxCalculationStrategy | null, taxAppId: string | null, displayGrossPrices: boolean, country: { __typename: 'CountryDisplay', country: string, code: string } }> } }> } | null }; export type TaxCountriesListQueryVariables = Exact<{ [key: string]: never; }>; @@ -12166,6 +12184,11 @@ export type TaxClassAssignQueryVariables = Exact<{ export type TaxClassAssignQuery = { __typename: 'Query', taxClasses: { __typename: 'TaxClassCountableConnection', edges: Array<{ __typename: 'TaxClassCountableEdge', node: { __typename: 'TaxClass', id: string, name: string } }>, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, endCursor: string | null } } | null }; +export type TaxStrategyChoicesQueryVariables = Exact<{ [key: string]: never; }>; + + +export type TaxStrategyChoicesQuery = { __typename: 'Query', shop: { __typename: 'Shop', availableTaxApps: Array<{ __typename: 'App', id: string, name: string | null, version: string | null, identifier: string | null, created: any | null, brand: { __typename: 'AppBrand', logo: { __typename: 'AppBrandLogo', default: string } } | null }> } }; + export type UpdateProductTranslationsMutationVariables = Exact<{ id: Scalars['ID']; input: TranslationInput; diff --git a/src/taxes/components/FlatTaxRateLabel.tsx b/src/taxes/components/FlatTaxRateLabel.tsx new file mode 100644 index 00000000000..206685fa40a --- /dev/null +++ b/src/taxes/components/FlatTaxRateLabel.tsx @@ -0,0 +1,18 @@ +import { Box, Text } from "@saleor/macaw-ui-next"; +import React from "react"; +import { useIntl } from "react-intl"; + +export const FlatTaxRateLabel: React.FC = () => { + const intl = useIntl(); + + return ( + + + {intl.formatMessage({ + defaultMessage: "Use flat tax rate", + id: "zSDfq0", + })} + + + ); +}; diff --git a/src/taxes/components/LegacyFlowWarning.tsx b/src/taxes/components/LegacyFlowWarning.tsx new file mode 100644 index 00000000000..6a3b1219878 --- /dev/null +++ b/src/taxes/components/LegacyFlowWarning.tsx @@ -0,0 +1,35 @@ +import { Box, Text, WarningIcon } from "@saleor/macaw-ui-next"; +import React from "react"; +import { useIntl } from "react-intl"; + +export const LegacyFlowWarning: React.FC<{ + taxCalculationStrategy: string; +}> = ({ taxCalculationStrategy }) => { + const intl = useIntl(); + + if (taxCalculationStrategy !== "legacy-flow") { + return null; + } + + return ( + + + + {intl.formatMessage({ + defaultMessage: + "Legacy flow detected - select tax strategy from dropdown", + id: "k20lqw", + })} + + + ); +}; diff --git a/src/taxes/components/PluginLabel.tsx b/src/taxes/components/PluginLabel.tsx new file mode 100644 index 00000000000..1b561a7b881 --- /dev/null +++ b/src/taxes/components/PluginLabel.tsx @@ -0,0 +1,18 @@ +import { Box, Text } from "@saleor/macaw-ui-next"; +import React from "react"; +import { useIntl } from "react-intl"; + +export const PluginLabel: React.FC = () => { + const intl = useIntl(); + + return ( + + + {intl.formatMessage({ + defaultMessage: "Use Avalara plugin", + id: "hyAOPB", + })} + + + ); +}; diff --git a/src/taxes/components/TaxAppLabel.tsx b/src/taxes/components/TaxAppLabel.tsx new file mode 100644 index 00000000000..c42aaaeffdc --- /dev/null +++ b/src/taxes/components/TaxAppLabel.tsx @@ -0,0 +1,87 @@ +import { AppAvatar } from "@dashboard/apps/components/AppAvatar/AppAvatar"; +import { AppUrls } from "@dashboard/apps/urls"; +import { Box, ExternalLinkIcon, Text } from "@saleor/macaw-ui-next"; +import moment from "moment"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +interface TaxAppLabelProps { + name: string | null; + logoUrl: string | undefined; + created: string | null; + version: string | null; + id: string; + identifier: string | null; +} + +export const TaxAppLabel: React.FC = ({ + name, + logoUrl, + created, + version, + id, + identifier, +}) => { + const logo = logoUrl ? { source: logoUrl } : undefined; + + return ( + + + + + + {name && ( + + {name}, + }} + /> + + )} + {version && ( + + {`v${version}`} + + )} + + {created && ( + + + + )} + + + + {identifier && ( + + {identifier} + + )} + + + + ); +}; diff --git a/src/taxes/components/index.ts b/src/taxes/components/index.ts new file mode 100644 index 00000000000..230cfa3d0fb --- /dev/null +++ b/src/taxes/components/index.ts @@ -0,0 +1,4 @@ +export * from "./FlatTaxRateLabel"; +export * from "./LegacyFlowWarning"; +export * from "./PluginLabel"; +export * from "./TaxAppLabel"; diff --git a/src/taxes/fixtures.ts b/src/taxes/fixtures.ts index e7e9ae282f5..52801d1eaee 100644 --- a/src/taxes/fixtures.ts +++ b/src/taxes/fixtures.ts @@ -18,6 +18,7 @@ export const taxConfigurations: TaxConfigurationFragment[] = [ pricesEnteredWithTax: false, chargeTaxes: true, taxCalculationStrategy: TaxCalculationStrategy.FLAT_RATES, + taxAppId: null, countries: [ { __typename: "TaxConfigurationPerCountry", @@ -29,6 +30,7 @@ export const taxConfigurations: TaxConfigurationFragment[] = [ chargeTaxes: false, taxCalculationStrategy: null, displayGrossPrices: false, + taxAppId: null, }, { __typename: "TaxConfigurationPerCountry", @@ -40,6 +42,7 @@ export const taxConfigurations: TaxConfigurationFragment[] = [ chargeTaxes: true, taxCalculationStrategy: TaxCalculationStrategy.TAX_APP, displayGrossPrices: true, + taxAppId: "42", }, ], }, @@ -55,6 +58,7 @@ export const taxConfigurations: TaxConfigurationFragment[] = [ pricesEnteredWithTax: true, chargeTaxes: true, taxCalculationStrategy: TaxCalculationStrategy.TAX_APP, + taxAppId: "42", countries: [ { __typename: "TaxConfigurationPerCountry", @@ -66,6 +70,7 @@ export const taxConfigurations: TaxConfigurationFragment[] = [ chargeTaxes: true, taxCalculationStrategy: TaxCalculationStrategy.FLAT_RATES, displayGrossPrices: true, + taxAppId: null, }, { __typename: "TaxConfigurationPerCountry", @@ -77,6 +82,7 @@ export const taxConfigurations: TaxConfigurationFragment[] = [ chargeTaxes: false, taxCalculationStrategy: null, displayGrossPrices: false, + taxAppId: null, }, ], }, diff --git a/src/taxes/pages/TaxChannelsPage/TaxChannelsPage.tsx b/src/taxes/pages/TaxChannelsPage/TaxChannelsPage.tsx index 39e3a44df31..3aa591f52b5 100644 --- a/src/taxes/pages/TaxChannelsPage/TaxChannelsPage.tsx +++ b/src/taxes/pages/TaxChannelsPage/TaxChannelsPage.tsx @@ -12,7 +12,6 @@ import { configurationMenuUrl } from "@dashboard/configuration"; import { CountryCode, CountryFragment, - TaxCalculationStrategy, TaxConfigurationFragment, TaxConfigurationPerCountryFragment, TaxConfigurationUpdateInput, @@ -35,10 +34,16 @@ import { Box, Button } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import { + getSelectedTaxStrategy, + getTaxAppId, + getTaxCalculationStrategy, +} from "./helpers"; import { useStyles } from "./styles"; import TaxChannelsMenu from "./TaxChannelsMenu"; import TaxCountryExceptionListItem from "./TaxCountryExceptionListItem"; import TaxSettingsCard from "./TaxSettingsCard"; +import { useTaxStrategyChoices } from "./useTaxStrategyChoices"; interface TaxChannelsPageProps { taxConfigurations: TaxConfigurationFragment[] | undefined; @@ -53,12 +58,19 @@ interface TaxChannelsPageProps { disabled: boolean; } +export type TaxCountryConfiguration = Omit< + TaxConfigurationPerCountryFragment, + "taxCalculationStrategy" +> & { + taxCalculationStrategy: string; +}; + export interface TaxConfigurationFormData { chargeTaxes: boolean; - taxCalculationStrategy: TaxCalculationStrategy; + taxCalculationStrategy: string; displayGrossPrices: boolean; pricesEnteredWithTax: boolean; - updateCountriesConfiguration: TaxConfigurationPerCountryFragment[]; + updateCountriesConfiguration: TaxCountryConfiguration[]; removeCountriesConfiguration: CountryCode[]; } @@ -80,62 +92,62 @@ export const TaxChannelsPage: React.FC = props => { const classes = useStyles(); const navigate = useNavigator(); + const { taxStrategyChoices, loading } = useTaxStrategyChoices(); + const currentTaxConfiguration = taxConfigurations?.find( taxConfigurations => taxConfigurations.id === selectedConfigurationId, ); const initialForm: TaxConfigurationFormData = { chargeTaxes: currentTaxConfiguration?.chargeTaxes ?? false, - taxCalculationStrategy: currentTaxConfiguration?.taxCalculationStrategy, + taxCalculationStrategy: getSelectedTaxStrategy(currentTaxConfiguration), displayGrossPrices: currentTaxConfiguration?.displayGrossPrices ?? false, pricesEnteredWithTax: currentTaxConfiguration?.pricesEnteredWithTax ?? false, - updateCountriesConfiguration: currentTaxConfiguration?.countries ?? [], + updateCountriesConfiguration: + currentTaxConfiguration?.countries.map(country => ({ + ...country, + taxCalculationStrategy: getSelectedTaxStrategy(country), + })) ?? [], removeCountriesConfiguration: [], }; const handleSubmit = (data: TaxConfigurationFormData) => { const { updateCountriesConfiguration, removeCountriesConfiguration } = data; + const parsedUpdate: TaxConfigurationUpdateInput["updateCountriesConfiguration"] = updateCountriesConfiguration.map(config => ({ countryCode: config.country.code as CountryCode, chargeTaxes: config.chargeTaxes, - taxCalculationStrategy: config.taxCalculationStrategy, + taxCalculationStrategy: getTaxCalculationStrategy( + config.taxCalculationStrategy, + ), displayGrossPrices: config.displayGrossPrices, + taxAppId: getTaxAppId(config.taxCalculationStrategy), })); const parsedRemove: TaxConfigurationUpdateInput["removeCountriesConfiguration"] = removeCountriesConfiguration.filter( configId => !parsedUpdate.some(config => config.countryCode === configId), ); + onSubmit({ chargeTaxes: data.chargeTaxes, taxCalculationStrategy: data.chargeTaxes - ? data.taxCalculationStrategy + ? getTaxCalculationStrategy(data.taxCalculationStrategy) : null, displayGrossPrices: data.displayGrossPrices, pricesEnteredWithTax: data.pricesEnteredWithTax, updateCountriesConfiguration: parsedUpdate, removeCountriesConfiguration: parsedRemove, + taxAppId: getTaxAppId(data.taxCalculationStrategy), }); }; - const taxStrategyChoices = [ - { - label: intl.formatMessage(taxesMessages.taxStrategyTaxApp), - value: TaxCalculationStrategy.TAX_APP, - }, - { - label: intl.formatMessage(taxesMessages.taxStrategyFlatRates), - value: TaxCalculationStrategy.FLAT_RATES, - }, - ]; - return (
{({ data, change, submit, set, triggerChange }) => { const countryExceptions = data.updateCountriesConfiguration; - const handleExceptionChange = (event, index) => { const { name, value } = event.target; const currentExceptions = [...data.updateCountriesConfiguration]; @@ -150,12 +162,13 @@ export const TaxChannelsPage: React.FC = props => { const handleCountryChange = (country: CountryFragment) => { closeDialog(); - const input: TaxConfigurationPerCountryFragment = { + const input: TaxCountryConfiguration = { __typename: "TaxConfigurationPerCountry", country, chargeTaxes: data.chargeTaxes, displayGrossPrices: data.displayGrossPrices, taxCalculationStrategy: data.taxCalculationStrategy, + taxAppId: getTaxAppId(data.taxCalculationStrategy), }; const currentExceptions = data.updateCountriesConfiguration; triggerChange(); @@ -199,6 +212,7 @@ export const TaxChannelsPage: React.FC = props => { values={data} strategyChoices={taxStrategyChoices} onChange={change} + strategyChoicesLoading={loading} /> @@ -226,7 +240,7 @@ export const TaxChannelsPage: React.FC = props => { /> ) : ( - + @@ -259,6 +273,7 @@ export const TaxChannelsPage: React.FC = props => { strategyChoices={taxStrategyChoices} country={country} key={country.country.code} + strategyChoicesLoading={loading} onDelete={() => { const currentRemovals = data.removeCountriesConfiguration; diff --git a/src/taxes/pages/TaxChannelsPage/TaxCountryExceptionListItem/TaxCountryExceptionListItem.tsx b/src/taxes/pages/TaxChannelsPage/TaxCountryExceptionListItem/TaxCountryExceptionListItem.tsx index 9570e4df46e..4baf3ad3224 100644 --- a/src/taxes/pages/TaxChannelsPage/TaxCountryExceptionListItem/TaxCountryExceptionListItem.tsx +++ b/src/taxes/pages/TaxChannelsPage/TaxCountryExceptionListItem/TaxCountryExceptionListItem.tsx @@ -3,30 +3,38 @@ import ControlledCheckbox from "@dashboard/components/ControlledCheckbox"; import SingleSelectField, { Choice, } from "@dashboard/components/SingleSelectField"; -import { - TaxConfigurationPerCountryFragment, - TaxConfigurationUpdateInput, -} from "@dashboard/graphql"; +import { TaxConfigurationUpdateInput } from "@dashboard/graphql"; import { FormChange } from "@dashboard/hooks/useForm"; +import { LegacyFlowWarning } from "@dashboard/taxes/components"; import { Divider } from "@material-ui/core"; import { ListItem, ListItemCell } from "@saleor/macaw-ui"; -import { Button, TrashBinIcon } from "@saleor/macaw-ui-next"; +import { Box, Button, TrashBinIcon } from "@saleor/macaw-ui-next"; import React from "react"; import { useStyles } from "../styles"; +import { TaxCountryConfiguration } from "../TaxChannelsPage"; interface TaxCountryExceptionListItemProps { - country: TaxConfigurationPerCountryFragment | undefined; + country: TaxCountryConfiguration | undefined; onDelete: () => void; onChange: FormChange; divider: boolean; strategyChoices: Choice[]; + strategyChoicesLoading: boolean; } export const TaxCountryExceptionListItem: React.FC< TaxCountryExceptionListItemProps -> = ({ country, onDelete, onChange, strategyChoices, divider = true }) => { +> = ({ + country, + onDelete, + onChange, + strategyChoices, + divider = true, + strategyChoicesLoading, +}) => { const classes = useStyles(); + return ( <> {country.country.country} - - - + + {!strategyChoicesLoading && ( + + )} + + + + = ({ values, strategyChoices, onChange, + strategyChoicesLoading, }) => { const intl = useIntl(); const classes = useStyles(); @@ -56,12 +59,17 @@ export const TaxSettingsCard: React.FC = ({ data-test-id="app-flat-select" > - {" "} + + {!strategyChoicesLoading && ( + + )} :first-child": { paddingTop: "2rem", }, }, singleSelectField: { - width: "275px", + // width: "275px", + // height: "32px", }, singleSelectWrapper: { display: "flex", flexDirection: "column", + flex: 1, }, hint: { marginLeft: 0, diff --git a/src/taxes/pages/TaxChannelsPage/helpers.test.ts b/src/taxes/pages/TaxChannelsPage/helpers.test.ts new file mode 100644 index 00000000000..2782904c9ed --- /dev/null +++ b/src/taxes/pages/TaxChannelsPage/helpers.test.ts @@ -0,0 +1,39 @@ +import { taxConfigurations } from "@dashboard/taxes/fixtures"; + +import { getSelectedTaxStrategy, getTaxAppId } from "./helpers"; + +describe("Tax Channels Page helpers", () => { + describe("getTaxAppId", () => { + it("should return id of tax app if strategy is not flat rate", () => { + const result = getTaxAppId("42"); + expect(result).toEqual("42"); + }); + + it("should return null if strategy is flat rate", () => { + const result = getTaxAppId("FLAT_RATES"); + expect(result).toEqual(null); + }); + }); + + describe("getSelectedTaxStrategy", () => { + const [flatRateConfiguration, taxAppConfiguration] = taxConfigurations; + + it("should return flat app strategy if strategy is one", () => { + const result = getSelectedTaxStrategy(flatRateConfiguration); + expect(result).toEqual("FLAT_RATES"); + }); + + it("should return id of tax app if strategy is not flat rate", () => { + const result = getSelectedTaxStrategy(taxAppConfiguration); + expect(result).toEqual("42"); + }); + + it("should return legacy-flow if strategy is not set - UI will show warning", () => { + const result = getSelectedTaxStrategy({ + ...taxAppConfiguration, + taxAppId: null, + }); + expect(result).toEqual("legacy-flow"); + }); + }); +}); diff --git a/src/taxes/pages/TaxChannelsPage/helpers.tsx b/src/taxes/pages/TaxChannelsPage/helpers.tsx new file mode 100644 index 00000000000..ad2290aff34 --- /dev/null +++ b/src/taxes/pages/TaxChannelsPage/helpers.tsx @@ -0,0 +1,25 @@ +import { + TaxCalculationStrategy, + TaxConfigurationFragment, + TaxConfigurationPerCountryFragment, +} from "@dashboard/graphql"; + +const isStrategyFlatRates = (strategy: string | null) => + strategy === TaxCalculationStrategy.FLAT_RATES; + +export const getTaxCalculationStrategy = (taxCalculationStrategy: string) => + isStrategyFlatRates(taxCalculationStrategy) + ? TaxCalculationStrategy.FLAT_RATES + : TaxCalculationStrategy.TAX_APP; + +export const getTaxAppId = (taxCalculationStrategy: string) => + isStrategyFlatRates(taxCalculationStrategy) ? null : taxCalculationStrategy; + +export const getSelectedTaxStrategy = ( + currentTaxConfiguration: + | TaxConfigurationFragment + | TaxConfigurationPerCountryFragment, +) => + isStrategyFlatRates(currentTaxConfiguration?.taxCalculationStrategy) + ? TaxCalculationStrategy.FLAT_RATES + : currentTaxConfiguration?.taxAppId ?? "legacy-flow"; diff --git a/src/taxes/pages/TaxChannelsPage/styles.ts b/src/taxes/pages/TaxChannelsPage/styles.ts index f8f762ff8a9..23fc49e8d45 100644 --- a/src/taxes/pages/TaxChannelsPage/styles.ts +++ b/src/taxes/pages/TaxChannelsPage/styles.ts @@ -8,6 +8,9 @@ export const useStyles = makeStyles( placeContent: "center", textAlign: "center", }, + cell: { + display: "grid", + }, left: { margin: 0, display: "flex", @@ -21,15 +24,14 @@ export const useStyles = makeStyles( }, noDivider: { "&::after, &::before": { display: "none" }, + display: "grid", + gridTemplateColumns: "1fr 500px 1fr 1fr", }, toolbarMargin: { "&:last-child": { marginRight: 0, }, }, - selectField: { - textAlign: "left", - }, }), { name: "TaxChannelsPage" }, ); diff --git a/src/taxes/pages/TaxChannelsPage/useTaxStrategyChoices.tsx b/src/taxes/pages/TaxChannelsPage/useTaxStrategyChoices.tsx new file mode 100644 index 00000000000..e9e8d3298a7 --- /dev/null +++ b/src/taxes/pages/TaxChannelsPage/useTaxStrategyChoices.tsx @@ -0,0 +1,44 @@ +import { + TaxCalculationStrategy, + useTaxStrategyChoicesQuery, +} from "@dashboard/graphql"; +import React from "react"; + +import { FlatTaxRateLabel, PluginLabel, TaxAppLabel } from "../../components"; + +const flatTaxRateChoice = { + label: , + value: TaxCalculationStrategy.FLAT_RATES, +}; + +const legacyPluginTaxChoice = { + label: , + value: "plugin:mirumee.taxes.avalara", +}; + +export const useTaxStrategyChoices = () => { + const { data, loading } = useTaxStrategyChoicesQuery(); + const taxAppsChoices = + data?.shop.availableTaxApps.map(app => ({ + value: app.identifier, + label: ( + + ), + })) ?? []; + + return { + taxStrategyChoices: [ + ...taxAppsChoices, + flatTaxRateChoice, + legacyPluginTaxChoice, + ], + loading, + }; +}; diff --git a/src/taxes/queries.ts b/src/taxes/queries.ts index 563802325c2..3c06202aaf2 100644 --- a/src/taxes/queries.ts +++ b/src/taxes/queries.ts @@ -74,3 +74,22 @@ export const taxClassAssign = gql` } } `; + +export const taxStrategyChoices = gql` + query TaxStrategyChoices { + shop { + availableTaxApps { + id + name + version + identifier + created + brand { + logo { + default + } + } + } + } + } +`;