From e8d6025374f23895f3d2a6497be281332a5d8d54 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Mon, 24 Jun 2024 17:25:44 +0200 Subject: [PATCH] Add support for tax inclusivity to region and store (#7808) This also includes rework of the currency model for the Store module. This change is breaking as existing stores won't have any supported currencies set, so users would need to go to the store settings again and choose the supported currencies there. --- .../helpers/seed-storefront-defaults.ts | 5 +- .../__tests__/product/store/product.spec.ts | 18 ++- .../http/__tests__/store/admin/store.spec.ts | 89 ++++++------ .../link-modules/store-currency.spec.ts | 26 ++-- .../__tests__/store/admin/store.spec.ts | 9 +- .../dashboard/src/i18n/translations/en.json | 3 +- .../create-campaign-form-fields.tsx | 11 +- .../create-shipping-options-prices-form.tsx | 2 +- .../edit-shipping-options-pricing-form.tsx | 2 +- .../pricing-prices-form.tsx | 4 +- .../pricing-products-prices-form.tsx | 2 +- .../products/common/variant-pricing-form.tsx | 4 +- .../product-create-variants-form.tsx | 6 +- .../rule-value-form-field.tsx | 2 +- .../create-region-form/create-region-form.tsx | 13 +- .../regions/region-create/region-create.tsx | 4 +- .../regions/region-edit/region-edit.tsx | 4 +- .../add-currencies-form.tsx | 16 ++- .../store-currency-section.tsx | 32 +++-- .../store-general-section.tsx | 8 +- .../edit-store-form/edit-store-form.tsx | 23 ++- .../dashboard/src/types/api-responses.ts | 5 +- .../defaults/steps/create-default-store.ts | 5 +- packages/core/types/src/bundles.ts | 2 +- packages/core/types/src/index.ts | 1 - packages/core/types/src/region/common.ts | 1 - .../core/types/src/region__legacy/common.ts | 51 ------- .../core/types/src/region__legacy/index.ts | 1 - packages/core/types/src/store/common/store.ts | 38 ++++- .../core/types/src/store/mutations/store.ts | 64 ++------- packages/core/types/src/store/service.ts | 13 +- .../src/api/admin/stores/query-config.ts | 7 +- .../medusa/src/api/admin/stores/validators.ts | 10 +- .../src/definitions/readonly/index.ts | 2 +- ...-default-currency.ts => store-currency.ts} | 6 +- .../migrations/.snapshot-medusa-region.json | 43 +++--- ...05173216.ts => Migration20240205173216.ts} | 0 .../integration-tests/__fixtures__/index.ts | 5 +- .../__tests__/store-module-service.spec.ts | 82 ++++++----- .../migrations/.snapshot-medusa-store.json | 131 +++++++++++++++--- .../src/migrations/Migration20240621145944.ts | 21 +++ packages/modules/store/src/models/currency.ts | 81 +++++++++++ packages/modules/store/src/models/index.ts | 1 + packages/modules/store/src/models/store.ts | 13 +- .../src/services/store-module-service.ts | 122 +++++++--------- 45 files changed, 580 insertions(+), 408 deletions(-) delete mode 100644 packages/core/types/src/region__legacy/common.ts delete mode 100644 packages/core/types/src/region__legacy/index.ts rename packages/modules/link-modules/src/definitions/readonly/{store-default-currency.ts => store-currency.ts} (72%) rename packages/modules/region/src/migrations/{RegionModuleSetup20240205173216.ts => Migration20240205173216.ts} (100%) create mode 100644 packages/modules/store/src/migrations/Migration20240621145944.ts create mode 100644 packages/modules/store/src/models/currency.ts diff --git a/integration-tests/helpers/seed-storefront-defaults.ts b/integration-tests/helpers/seed-storefront-defaults.ts index 6945ae80d8..99161e84c9 100644 --- a/integration-tests/helpers/seed-storefront-defaults.ts +++ b/integration-tests/helpers/seed-storefront-defaults.ts @@ -29,8 +29,9 @@ export const seedStorefrontDefaults = async ( store = await storeModule.updateStores(store.id, { default_region_id: region.id, - supported_currency_codes: [region.currency_code], - default_currency_code: region.currency_code, + supported_currencies: [ + { currency_code: region.currency_code, is_default: true }, + ], }) return { diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index de3aab92b7..5237fa0680 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -97,8 +97,10 @@ medusaIntegrationTestRunner({ store = await storeModule.createStores({ name: "New store", - supported_currency_codes: ["usd", "dkk"], - default_currency_code: "usd", + supported_currencies: [ + { currency_code: "usd", is_default: true }, + { currency_code: "dkk" }, + ], }) }) @@ -570,8 +572,10 @@ medusaIntegrationTestRunner({ } await service.createStores({ - supported_currency_codes: ["usd", "dkk"], - default_currency_code: "usd", + supported_currencies: [ + { currency_code: "usd", is_default: true }, + { currency_code: "dkk" }, + ], default_sales_channel_id: defaultSalesChannel.id, }) }) @@ -1139,8 +1143,10 @@ medusaIntegrationTestRunner({ } await service.createStores({ - supported_currency_codes: ["usd", "dkk"], - default_currency_code: "usd", + supported_currencies: [ + { currency_code: "usd", is_default: true }, + { currency_code: "dkk" }, + ], default_sales_channel_id: defaultSalesChannel.id, }) }) diff --git a/integration-tests/http/__tests__/store/admin/store.spec.ts b/integration-tests/http/__tests__/store/admin/store.spec.ts index d75185695f..7805cabb2f 100644 --- a/integration-tests/http/__tests__/store/admin/store.spec.ts +++ b/integration-tests/http/__tests__/store/admin/store.spec.ts @@ -30,8 +30,10 @@ medusaIntegrationTestRunner({ store = await storeModule.createStores({ name: "New store", - supported_currency_codes: ["usd", "dkk"], - default_currency_code: "usd", + supported_currencies: [ + { currency_code: "usd", is_default: true }, + { currency_code: "dkk" }, + ], default_sales_channel_id: "sc_12345", }) }) @@ -48,9 +50,14 @@ medusaIntegrationTestRunner({ expect.objectContaining({ id: expect.any(String), name: "New store", - default_currency_code: "usd", default_sales_channel_id: expect.any(String), - supported_currency_codes: ["usd", "dkk"], + supported_currencies: [ + expect.objectContaining({ + currency_code: "usd", + is_default: true, + }), + expect.objectContaining({ currency_code: "dkk" }), + ], created_at: expect.any(String), updated_at: expect.any(String), }) @@ -59,12 +66,12 @@ medusaIntegrationTestRunner({ }) describe("POST /admin/stores", () => { - it("fails to update default currency if not in store currencies", async () => { + it("fails to update default currencies if there is no default one", async () => { const err = await api .post( `/admin/stores/${store.id}`, { - default_currency_code: "eur", + supported_currencies: [{ currency_code: "eur" }], }, adminHeaders ) @@ -74,17 +81,19 @@ medusaIntegrationTestRunner({ expect(err.response.data).toEqual( expect.objectContaining({ type: "invalid_data", - message: "Store does not have currency: eur", + message: "There should be a default currency set for the store", }) ) }) - // BREAKING: `currencies` was renamed to `supported_currency_codes` + // BREAKING: `currencies` was renamed to `supported_currencies` it("fails to remove default currency from currencies without replacing it", async () => { const err = await api .post( `/admin/stores/${store.id}`, - { supported_currency_codes: ["dkk"] }, + { + supported_currencies: [{ currency_code: "dkk" }], + }, adminHeaders ) .catch((e) => e) @@ -93,41 +102,19 @@ medusaIntegrationTestRunner({ expect(err.response.data).toEqual( expect.objectContaining({ type: "invalid_data", - message: - "You are not allowed to remove default currency from store currencies without replacing it as well", + message: "There should be a default currency set for the store", }) ) }) - it("successfully updates default currency code", async () => { - const response = await api - .post( - `/admin/stores/${store.id}`, - { - default_currency_code: "dkk", - }, - adminHeaders - ) - .catch((err) => console.log(err)) - - expect(response.status).toEqual(200) - expect(response.data.store).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "New store", - default_currency_code: "dkk", - created_at: expect.any(String), - updated_at: expect.any(String), - }) - ) - }) - - it("successfully updates default currency and store currencies", async () => { + it("successfully updates currencies and default currency", async () => { const response = await api.post( `/admin/stores/${store.id}`, { - default_currency_code: "jpy", - supported_currency_codes: ["jpy", "usd"], + supported_currencies: [ + { currency_code: "usd" }, + { currency_code: "dkk", is_default: true }, + ], }, adminHeaders ) @@ -137,9 +124,15 @@ medusaIntegrationTestRunner({ expect.objectContaining({ id: expect.any(String), name: "New store", - default_sales_channel_id: expect.any(String), - supported_currency_codes: ["jpy", "usd"], - default_currency_code: "jpy", + supported_currencies: [ + expect.objectContaining({ + currency_code: "usd", + }), + expect.objectContaining({ + currency_code: "dkk", + is_default: true, + }), + ], created_at: expect.any(String), updated_at: expect.any(String), }) @@ -150,7 +143,10 @@ medusaIntegrationTestRunner({ const response = await api.post( `/admin/stores/${store.id}`, { - supported_currency_codes: ["jpy", "usd"], + supported_currencies: [ + { currency_code: "jpy", is_default: true }, + { currency_code: "usd" }, + ], }, adminHeaders ) @@ -161,8 +157,15 @@ medusaIntegrationTestRunner({ id: expect.any(String), default_sales_channel_id: expect.any(String), name: "New store", - supported_currency_codes: ["jpy", "usd"], - default_currency_code: "usd", + supported_currencies: [ + expect.objectContaining({ + currency_code: "jpy", + is_default: true, + }), + expect.objectContaining({ + currency_code: "usd", + }), + ], created_at: expect.any(String), updated_at: expect.any(String), }) diff --git a/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts b/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts index 290e3ecb49..ec9e92a426 100644 --- a/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts +++ b/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts @@ -1,5 +1,6 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ICurrencyModuleService, IStoreModuleService } from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" jest.setTimeout(50000) @@ -27,24 +28,29 @@ medusaIntegrationTestRunner({ it("should query store and default currency with remote query", async () => { const store = await storeModuleService.createStores({ name: "Store", - default_currency_code: "usd", - supported_currency_codes: ["usd"], + supported_currencies: [{ currency_code: "usd", is_default: true }], }) - const stores = await remoteQuery({ - store: { - fields: ["id"], - default_currency: { - fields: ["code"], - }, - }, + const query = remoteQueryObjectFromString({ + entryPoint: "store", + fields: [ + "id", + "supported_currencies.*", + "supported_currencies.currency.*", + ], }) + const stores = await remoteQuery(query) expect(stores).toEqual( expect.arrayContaining([ expect.objectContaining({ id: store.id, - default_currency: expect.objectContaining({ code: "usd" }), + supported_currencies: expect.arrayContaining([ + expect.objectContaining({ + currency: expect.objectContaining({ code: "usd" }), + currency_code: "usd", + }), + ]), }), ]) ) diff --git a/integration-tests/modules/__tests__/store/admin/store.spec.ts b/integration-tests/modules/__tests__/store/admin/store.spec.ts index cb276625bb..ccaafb8fe9 100644 --- a/integration-tests/modules/__tests__/store/admin/store.spec.ts +++ b/integration-tests/modules/__tests__/store/admin/store.spec.ts @@ -29,13 +29,18 @@ medusaIntegrationTestRunner({ it("should correctly implement the entire lifecycle of a store", async () => { const createdStore = await service.createStores({ name: "Test store", - supported_currency_codes: ["usd"], + supported_currencies: [{ currency_code: "usd", is_default: true }], }) expect(createdStore).toEqual( expect.objectContaining({ id: createdStore.id, - supported_currency_codes: ["usd"], + supported_currencies: [ + expect.objectContaining({ + currency_code: "usd", + is_default: true, + }), + ], name: "Test store", }) ) diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 29e0e42fc1..79b82564ae 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -848,7 +848,6 @@ "shippingProfilesDesc": "Shipping rules for different types of products", "shippingOptionTypes": "Shipping Option Types", "shippingOptionTypesDesc": "Group options based on characteristic" - }, "salesChannels": { "header": "Sales Channels", @@ -1422,6 +1421,7 @@ "removeCountriesWarning_one": "You are about to remove {{count}} country from the region. This action cannot be undone.", "removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.", "removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.", + "automaticTaxesHint": "When enabled taxes will only be calculated at checkout based on the shipping address.", "taxInclusiveHint": "When enabled prices in the region will be tax inclusive.", "providersHint": " Add which payment providers should be available in this region.", "shippingOptions": "Shipping Options", @@ -1791,6 +1791,7 @@ "inventory": "Inventory", "optional": "Optional", "note": "Note", + "automaticTaxes": "Automatic Taxes", "taxInclusivePricing": "Tax inclusive pricing", "taxRate": "Tax Rate", "taxCode": "Tax Code", diff --git a/packages/admin-next/dashboard/src/routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx b/packages/admin-next/dashboard/src/routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx index 55f9529a59..bf6f483262 100644 --- a/packages/admin-next/dashboard/src/routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx +++ b/packages/admin-next/dashboard/src/routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx @@ -248,10 +248,13 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => { {Object.values(currencies) - .filter((currency) => - store?.supported_currency_codes?.includes( - currency.code.toLocaleLowerCase() - ) + .filter( + (currency) => + !!store?.supported_currencies?.find( + (c) => + c.currency_code === + currency.code.toLocaleLowerCase() + ) ) .map((currency) => ( store?.supported_currency_codes || [], + () => store?.supported_currencies?.map((c) => c.currency_code) || [], [store] ) diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx index 9e583fc2e8..099196ff7a 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx @@ -94,7 +94,7 @@ export function EditShippingOptionsPricingForm({ } = useStore() const currencies = useMemo( - () => store?.supported_currency_codes || [], + () => store?.supported_currencies?.map((c) => c.currency_code) || [], [store] ) diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-create/components/pricing-create-form/pricing-prices-form.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-create/components/pricing-create-form/pricing-prices-form.tsx index bb773b51c0..bc47f70444 100644 --- a/packages/admin-next/dashboard/src/routes/pricing/pricing-create/components/pricing-create-form/pricing-prices-form.tsx +++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-create/components/pricing-create-form/pricing-prices-form.tsx @@ -28,8 +28,8 @@ export const PricingPricesForm = ({ form }: PricingPricesFormProps) => { error: currencyError, } = useCurrencies( { - code: store?.supported_currency_codes, - limit: store?.supported_currency_codes?.length, + code: store?.supported_currencies?.map((c) => c.currency_code), + limit: store?.supported_currencies?.length, }, { enabled: !!store, diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-prices/components/pricing-products-prices-form/pricing-products-prices-form.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-prices/components/pricing-products-prices-form/pricing-products-prices-form.tsx index 1cc1eac1fd..a0b09e926c 100644 --- a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-prices/components/pricing-products-prices-form/pricing-products-prices-form.tsx +++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-prices/components/pricing-products-prices-form/pricing-products-prices-form.tsx @@ -97,7 +97,7 @@ export const PricingProductPricesForm = ({ error: currencyError, } = useCurrencies( { - code: store?.supported_currency_codes, + code: store?.supported_currencies?.map((c) => c.currency_code), }, { enabled: !!store, diff --git a/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx b/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx index e827594e23..298fc5c733 100644 --- a/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx @@ -19,8 +19,8 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => { const { store, isLoading: isStoreLoading } = useStore() const { currencies, isLoading: isCurrenciesLoading } = useCurrencies( { - code: store?.supported_currency_codes, - limit: store?.supported_currency_codes?.length, + code: store?.supported_currencies?.map((c) => c.currency_code), + limit: store?.supported_currencies?.length, }, { enabled: !!store, diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx index 4747bf02d9..a09338caa3 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx @@ -21,9 +21,7 @@ export const ProductCreateVariantsForm = ({ }: ProductCreateVariantsFormProps) => { const { regions } = useRegions({ limit: 9999 }) - const { store, isPending, isError, error } = useStore({ - fields: "supported_currency_codes", - }) + const { store, isPending, isError, error } = useStore() const variants = useWatch({ control: form.control, @@ -39,7 +37,7 @@ export const ProductCreateVariantsForm = ({ const columns = useColumns({ options, - currencies: store?.supported_currency_codes, + currencies: store?.supported_currencies?.map((c) => c.currency_code) || [], regions, }) diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rule-value-form-field/rule-value-form-field.tsx b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rule-value-form-field/rule-value-form-field.tsx index f3f43a3a91..b4ebb6d65f 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rule-value-form-field/rule-value-form-field.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rule-value-form-field/rule-value-form-field.tsx @@ -28,7 +28,7 @@ const buildFilters = (attribute?: string, store?: StoreDTO) => { if (attribute === "currency_code") { return { - value: store.supported_currency_codes, + value: store.supported_currencies?.map((c) => c.currency_code), } } diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx b/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx index a5ab417f71..232c3d2d67 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx @@ -44,7 +44,7 @@ type CreateRegionFormProps = { const CreateRegionSchema = zod.object({ name: zod.string().min(1), currency_code: zod.string().min(2, "Select a currency"), - includes_tax: zod.boolean(), + automatic_taxes: zod.boolean(), countries: zod.array(zod.object({ code: zod.string(), name: zod.string() })), payment_providers: zod.array(zod.string()).min(1), }) @@ -64,7 +64,7 @@ export const CreateRegionForm = ({ defaultValues: { name: "", currency_code: "", - includes_tax: false, + automatic_taxes: true, countries: [], payment_providers: [], }, @@ -88,7 +88,7 @@ export const CreateRegionForm = ({ countries: values.countries.map((c) => c.code), currency_code: values.currency_code, payment_providers: values.payment_providers, - automatic_taxes: values.includes_tax, + automatic_taxes: values.automatic_taxes, }, { onSuccess: ({ region }) => { @@ -277,14 +277,14 @@ export const CreateRegionForm = ({ { return (
- {t("fields.taxInclusivePricing")} + {t("fields.automaticTaxes")}
- {t("regions.taxInclusiveHint")} + {t("regions.automaticTaxesHint")}
@@ -303,6 +303,7 @@ export const CreateRegionForm = ({ ) }} /> +
diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx index 0c74f803b4..ee3c38f686 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx @@ -7,8 +7,8 @@ import { useStore } from "../../../hooks/api/store" export const RegionCreate = () => { const { store, isPending: isLoading, isError, error } = useStore() - const storeCurrencies = (store?.supported_currency_codes ?? []).map( - (code) => currencies[code.toUpperCase()] + const storeCurrencies = (store?.supported_currencies ?? []).map( + (c) => currencies[c.currency_code.toUpperCase()] ) const { payment_providers: paymentProviders = [] } = usePaymentProviders() diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx index 0f98f3b894..3d233818ed 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx @@ -29,8 +29,8 @@ export const RegionEdit = () => { const isLoading = isRegionLoading || isStoreLoading - const storeCurrencies = (store?.supported_currency_codes ?? []).map( - (code) => currencies[code.toUpperCase()] + const storeCurrencies = (store?.supported_currencies ?? []).map( + (c) => currencies[c.currency_code.toUpperCase()] ) const { payment_providers: paymentProviders = [] } = usePaymentProviders({ limit: 999, diff --git a/packages/admin-next/dashboard/src/routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx b/packages/admin-next/dashboard/src/routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx index e26e484bcd..41f3a35276 100644 --- a/packages/admin-next/dashboard/src/routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx +++ b/packages/admin-next/dashboard/src/routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx @@ -78,7 +78,8 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => { placeholderData: keepPreviousData, }) - const preSelectedRows = store.supported_currency_codes.map((c) => c) + const preSelectedRows = + store.supported_currencies?.map((c) => c.currency_code) ?? [] const columns = useColumns() @@ -104,9 +105,20 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => { new Set([...data.currencies, ...preSelectedRows]) ) as string[] + let defaultCurrency = store.supported_currencies?.find( + (c) => c.is_default + )?.currency_code + + if (!currencies.includes(defaultCurrency ?? "")) { + defaultCurrency = currencies?.[0] + } + try { await mutateAsync({ - supported_currency_codes: currencies, + supported_currencies: currencies.map((c) => ({ + currency_code: c, + is_default: c === defaultCurrency, + })), }) toast.success(t("general.success"), { description: t("store.toast.currenciesUpdated"), diff --git a/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx b/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx index e05e1a0043..5967952f65 100644 --- a/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx +++ b/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx @@ -1,5 +1,5 @@ import { Plus, Trash } from "@medusajs/icons" -import { CurrencyDTO } from "@medusajs/types" +import { CurrencyDTO, StoreCurrencyDTO } from "@medusajs/types" import { Checkbox, CommandBar, @@ -40,7 +40,7 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => { error, } = useCurrencies( { - code: store.supported_currency_codes, + code: store.supported_currencies?.map((c) => c.currency_code), ...searchParams, }, { @@ -64,8 +64,9 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => { pageSize: PAGE_SIZE, meta: { storeId: store.id, - currencyCodes: store.supported_currency_codes, - defaultCurrencyCode: store.default_currency_code, + supportedCurrencies: store.supported_currencies, + defaultCurrencyCode: store.supported_currencies?.find((c) => c.is_default) + ?.currency_code, }, }) @@ -91,9 +92,10 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => { try { await mutateAsync({ - supported_currency_codes: store.supported_currency_codes.filter( - (c) => !ids.includes(c) - ), + supported_currencies: + store.supported_currencies?.filter( + (c) => !ids.includes(c.currency_code) + ) ?? [], }) setRowSelection({}) @@ -164,12 +166,12 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => { const CurrencyActions = ({ storeId, currency, - currencyCodes, + supportedCurrencies, defaultCurrencyCode, }: { storeId: string currency: CurrencyDTO - currencyCodes: string[] + supportedCurrencies: StoreCurrencyDTO[] defaultCurrencyCode: string }) => { const { mutateAsync } = useUpdateStore(storeId) @@ -195,8 +197,8 @@ const CurrencyActions = ({ try { await mutateAsync({ - supported_currency_codes: currencyCodes.filter( - (c) => c !== currency.code + supported_currencies: supportedCurrencies.filter( + (c) => c.currency_code !== currency.code ), }) @@ -269,9 +271,9 @@ const useColumns = () => { columnHelper.display({ id: "actions", cell: ({ row, table }) => { - const { currencyCodes, storeId, defaultCurrencyCode } = table.options - .meta as { - currencyCodes: string[] + const { supportedCurrencies, storeId, defaultCurrencyCode } = table + .options.meta as { + supportedCurrencies: StoreCurrencyDTO[] storeId: string defaultCurrencyCode: string } @@ -280,7 +282,7 @@ const useColumns = () => { ) diff --git a/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-general-section/store-general-section.tsx b/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-general-section/store-general-section.tsx index 312b35a4c8..55320335af 100644 --- a/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-general-section/store-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/store/store-detail/components/store-general-section/store-general-section.tsx @@ -16,6 +16,8 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => { enabled: !!store.default_region_id, }) + const defaultCurrency = store.supported_currencies?.find((c) => c.is_default) + return (
@@ -51,13 +53,13 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => { {t("store.defaultCurrency")} - {store.default_currency ? ( + {defaultCurrency ? (
- {store.default_currency.code.toUpperCase()} + {defaultCurrency.currency_code.toUpperCase()} - {store.default_currency.name} + {defaultCurrency.currency.name}
) : ( diff --git a/packages/admin-next/dashboard/src/routes/store/store-edit/components/edit-store-form/edit-store-form.tsx b/packages/admin-next/dashboard/src/routes/store/store-edit/components/edit-store-form/edit-store-form.tsx index df9999953e..59f9f09d4c 100644 --- a/packages/admin-next/dashboard/src/routes/store/store-edit/components/edit-store-form/edit-store-form.tsx +++ b/packages/admin-next/dashboard/src/routes/store/store-edit/components/edit-store-form/edit-store-form.tsx @@ -32,7 +32,9 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => { defaultValues: { name: store.name, default_region_id: store.default_region_id || undefined, - default_currency_code: store.default_currency_code || undefined, + default_currency_code: + store.supported_currencies?.find((c) => c.is_default)?.currency_code || + undefined, }, resolver: zodResolver(EditStoreSchema), }) @@ -43,7 +45,15 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => { const handleSubmit = form.handleSubmit(async (values) => { try { - await mutateAsync(values) + const normalizedMutation = { + ...values, + default_currency_code: undefined, + supported_currencies: store.supported_currencies?.map((c) => ({ + ...c, + is_default: c.currency_code === values.default_currency_code, + })), + } + await mutateAsync(normalizedMutation) handleSuccess() @@ -91,9 +101,12 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => { - {store.supported_currency_codes.map((code) => ( - - {code.toUpperCase()} + {store.supported_currencies?.map((currency) => ( + + {currency.currency_code.toUpperCase()} ))} diff --git a/packages/admin-next/dashboard/src/types/api-responses.ts b/packages/admin-next/dashboard/src/types/api-responses.ts index 09bb540a30..33870c77cb 100644 --- a/packages/admin-next/dashboard/src/types/api-responses.ts +++ b/packages/admin-next/dashboard/src/types/api-responses.ts @@ -22,7 +22,6 @@ import { StockLocationDTO, StoreDTO, UserDTO, - HttpTypes, } from "@medusajs/types" import { WorkflowExecutionDTO } from "../routes/workflow-executions/types" @@ -57,9 +56,7 @@ export type UserListRes = { users: UserDTO[] } & ListRes export type UserDeleteRes = DeleteRes // Stores -export type ExtendedStoreDTO = StoreDTO & { - default_currency: CurrencyDTO | null -} +export type ExtendedStoreDTO = StoreDTO export type StoreRes = { store: ExtendedStoreDTO } export type StoreListRes = { stores: ExtendedStoreDTO[] } & ListRes diff --git a/packages/core/core-flows/src/defaults/steps/create-default-store.ts b/packages/core/core-flows/src/defaults/steps/create-default-store.ts index 0cbde958ad..9dbc97039e 100644 --- a/packages/core/core-flows/src/defaults/steps/create-default-store.ts +++ b/packages/core/core-flows/src/defaults/steps/create-default-store.ts @@ -28,8 +28,9 @@ export const createDefaultStoreStep = createStep( { // TODO: Revisit for a more sophisticated approach ...data.store, - supported_currency_codes: ["eur"], - default_currency_code: "eur", + supported_currencies: [ + { currency_code: "eur", is_default: true }, + ], }, ], }, diff --git a/packages/core/types/src/bundles.ts b/packages/core/types/src/bundles.ts index 8c096c7c02..abedf15d2f 100644 --- a/packages/core/types/src/bundles.ts +++ b/packages/core/types/src/bundles.ts @@ -20,7 +20,7 @@ export * as OrderTypes from "./order" export * as PricingTypes from "./pricing" export * as ProductTypes from "./product" export * as PromotionTypes from "./promotion" -export * as RegionTypes from "./region__legacy" +export * as RegionTypes from "./region" export * as SalesChannelTypes from "./sales-channel" export * as SearchTypes from "./search" export * as StockLocationTypes from "./stock-location" diff --git a/packages/core/types/src/index.ts b/packages/core/types/src/index.ts index d294c40c31..f7fd908b4e 100644 --- a/packages/core/types/src/index.ts +++ b/packages/core/types/src/index.ts @@ -28,7 +28,6 @@ export * from "./product" export * from "./product-category" export * from "./promotion" export * from "./region" -export * from "./region__legacy" export * from "./sales-channel" export * from "./search" export * from "./shared-context" diff --git a/packages/core/types/src/region/common.ts b/packages/core/types/src/region/common.ts index 9800a3e384..cb4d98f955 100644 --- a/packages/core/types/src/region/common.ts +++ b/packages/core/types/src/region/common.ts @@ -24,7 +24,6 @@ export interface RegionDTO { * Setting to indicate whether taxes need to be applied automatically */ automatic_taxes: boolean - /** * The countries of the region. */ diff --git a/packages/core/types/src/region__legacy/common.ts b/packages/core/types/src/region__legacy/common.ts deleted file mode 100644 index a82fca556a..0000000000 --- a/packages/core/types/src/region__legacy/common.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @interface - * - * The details of a legacy region. - */ -export type RegionDTO__legacy = { - /** - * The name of the region. - */ - name: string - - /** - * The currency code of the region. - */ - currency_code: string - - /** - * The tax rate of the region. - */ - tax_rate?: number - - /** - * The tax code of the region. - */ - tax_code?: string | null - - /** - * Whether gift cards in the region are taxable. - */ - gift_cards_taxable?: boolean - - /** - * Whether taxes should be calculated automatically in the region. - */ - automatic_taxes?: boolean - - /** - * The associated tax provider's ID. - */ - tax_provider_id?: string | null - - /** - * Holds custom data in key-value pairs. - */ - metadata?: Record - - /** - * Whether prices include taxes in the region. - */ - includes_tax?: boolean -} diff --git a/packages/core/types/src/region__legacy/index.ts b/packages/core/types/src/region__legacy/index.ts deleted file mode 100644 index 488a94fdff..0000000000 --- a/packages/core/types/src/region__legacy/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./common" diff --git a/packages/core/types/src/store/common/store.ts b/packages/core/types/src/store/common/store.ts index 1f0f242c45..98096446af 100644 --- a/packages/core/types/src/store/common/store.ts +++ b/packages/core/types/src/store/common/store.ts @@ -1,5 +1,36 @@ import { BaseFilterable } from "../../dal" +export interface StoreCurrencyDTO { + /** + * The ID of the store currency. + */ + id: string + /** + * The currency code of the store currency. + */ + currency_code: string + /** + * Whether the currency is the default one for the store. + */ + is_default: boolean + /** + * The store ID associated with the currency. + */ + store_id: string + /** + * The created date of the currency + */ + created_at: string + /** + * The updated date of the currency + */ + updated_at: string + /** + * The deleted date of the currency + */ + deleted_at: string | null +} + /** * The store details. */ @@ -17,12 +48,7 @@ export interface StoreDTO { /** * The supported currency codes of the store. */ - supported_currency_codes: string[] - - /** - * The default currency code of the store. - */ - default_currency_code?: string + supported_currencies?: StoreCurrencyDTO[] /** * The associated default sales channel's ID. diff --git a/packages/core/types/src/store/mutations/store.ts b/packages/core/types/src/store/mutations/store.ts index 364cdaf777..d978ee8504 100644 --- a/packages/core/types/src/store/mutations/store.ts +++ b/packages/core/types/src/store/mutations/store.ts @@ -1,3 +1,14 @@ +export interface CreateStoreCurrencyDTO { + /** + * The currency code of the store currency. + */ + currency_code: string + /** + * Whether the currency is the default one for the store. + */ + is_default?: boolean +} + /** * The store to be created. */ @@ -10,12 +21,7 @@ export interface CreateStoreDTO { /** * The supported currency codes of the store. */ - supported_currency_codes?: string[] - - /** - * The default currency code of the store. - */ - default_currency_code?: string + supported_currencies?: CreateStoreCurrencyDTO[] /** * The associated default sales channel's ID. @@ -41,46 +47,11 @@ export interface CreateStoreDTO { /** * The attributes in the store to be created or updated. */ -export interface UpsertStoreDTO { +export interface UpsertStoreDTO extends UpdateStoreDTO { /** - * The ID of the store. + * The ID of the store when doing an update. */ id?: string - - /** - * The name of the store. - */ - name?: string - - /** - * The supported currency codes of the store. - */ - supported_currency_codes?: string[] - - /** - * The default currency code of the store. - */ - default_currency_code?: string - - /** - * The associated default sales channel's ID. - */ - default_sales_channel_id?: string - - /** - * The associated default region's ID. - */ - default_region_id?: string - - /** - * The associated default location's ID. - */ - default_location_id?: string - - /** - * Holds custom data in key-value pairs. - */ - metadata?: Record } /** @@ -95,12 +66,7 @@ export interface UpdateStoreDTO { /** * The supported currency codes of the store. */ - supported_currency_codes?: string[] - - /** - * The default currency code of the store. - */ - default_currency_code?: string + supported_currencies?: CreateStoreCurrencyDTO[] /** * The associated default sales channel's ID. diff --git a/packages/core/types/src/store/service.ts b/packages/core/types/src/store/service.ts index 24074d3c6a..5b761521e9 100644 --- a/packages/core/types/src/store/service.ts +++ b/packages/core/types/src/store/service.ts @@ -20,13 +20,11 @@ export interface IStoreModuleService extends IModuleService { * const stores = await storeModuleService.createStores([ * { * name: "Acme", - * supported_currency_codes: ["usd", "eur"], - * default_currency_code: "usd", + * supported_currencies: [{ currency_code: "usd", is_default: true }, { currency_code: "eur" }], * }, * { * name: "Acme 2", - * supported_currency_codes: ["usd"], - * default_currency_code: "usd", + * supported_currencies: [{currency_code: "usd", is_default: true}], * }, * ]) */ @@ -45,8 +43,7 @@ export interface IStoreModuleService extends IModuleService { * @example * const store = await storeModuleService.createStores({ * name: "Acme", - * supported_currency_codes: ["usd", "eur"], - * default_currency_code: "usd", + * supported_currencies: [{ currency_code: "usd", is_default: true }, { currency_code: "eur" }], * }) */ createStores(data: CreateStoreDTO, sharedContext?: Context): Promise @@ -66,8 +63,7 @@ export interface IStoreModuleService extends IModuleService { * }, * { * name: "Acme 2", - * supported_currency_codes: ["usd"], - * default_currency_code: "usd", + * supported_currencies: [{currency_code: "usd", is_default: true}], * }, * ]) */ @@ -124,7 +120,6 @@ export interface IStoreModuleService extends IModuleService { * name: ["Acme"], * }, * { - * default_currency_code: "usd", * } * ) */ diff --git a/packages/medusa/src/api/admin/stores/query-config.ts b/packages/medusa/src/api/admin/stores/query-config.ts index 55943d21f6..5a53afc3c6 100644 --- a/packages/medusa/src/api/admin/stores/query-config.ts +++ b/packages/medusa/src/api/admin/stores/query-config.ts @@ -1,11 +1,8 @@ export const defaultAdminStoreFields = [ "id", "name", - "supported_currency_codes", - "default_currency_code", - "default_currency.name", - "default_currency.symbol", - "default_currency.symbol_native", + "*supported_currencies", + "*supported_currencies.currency", "default_sales_channel_id", "default_region_id", "default_location_id", diff --git a/packages/medusa/src/api/admin/stores/validators.ts b/packages/medusa/src/api/admin/stores/validators.ts index f374b731c5..3e14142b96 100644 --- a/packages/medusa/src/api/admin/stores/validators.ts +++ b/packages/medusa/src/api/admin/stores/validators.ts @@ -21,8 +21,14 @@ export const AdminGetStoresParams = createFindParams({ export type AdminUpdateStoreType = z.infer export const AdminUpdateStore = z.object({ name: z.string().nullish(), - supported_currency_codes: z.array(z.string()).optional(), - default_currency_code: z.string().nullish(), + supported_currencies: z + .array( + z.object({ + currency_code: z.string(), + is_default: z.boolean().optional(), + }) + ) + .optional(), default_sales_channel_id: z.string().nullish(), default_region_id: z.string().nullish(), default_location_id: z.string().nullish(), diff --git a/packages/modules/link-modules/src/definitions/readonly/index.ts b/packages/modules/link-modules/src/definitions/readonly/index.ts index 28d71d6f74..379011505e 100644 --- a/packages/modules/link-modules/src/definitions/readonly/index.ts +++ b/packages/modules/link-modules/src/definitions/readonly/index.ts @@ -8,4 +8,4 @@ export * from "./order-customer" export * from "./order-product" export * from "./order-region" export * from "./order-sales-channel" -export * from "./store-default-currency" +export * from "./store-currency" diff --git a/packages/modules/link-modules/src/definitions/readonly/store-default-currency.ts b/packages/modules/link-modules/src/definitions/readonly/store-currency.ts similarity index 72% rename from packages/modules/link-modules/src/definitions/readonly/store-default-currency.ts rename to packages/modules/link-modules/src/definitions/readonly/store-currency.ts index d0018ef68b..ab04fa274a 100644 --- a/packages/modules/link-modules/src/definitions/readonly/store-default-currency.ts +++ b/packages/modules/link-modules/src/definitions/readonly/store-currency.ts @@ -1,7 +1,7 @@ import { ModuleJoinerConfig } from "@medusajs/types" import { Modules } from "@medusajs/utils" -export const StoreDefaultCurrency: ModuleJoinerConfig = { +export const StoreCurrencies: ModuleJoinerConfig = { isLink: true, isReadOnlyLink: true, extends: [ @@ -10,8 +10,8 @@ export const StoreDefaultCurrency: ModuleJoinerConfig = { relationship: { serviceName: Modules.CURRENCY, primaryKey: "code", - foreignKey: "default_currency_code", - alias: "default_currency", + foreignKey: "supported_currencies.currency_code", + alias: "currency", args: { methodSuffix: "Currencies", }, diff --git a/packages/modules/region/src/migrations/.snapshot-medusa-region.json b/packages/modules/region/src/migrations/.snapshot-medusa-region.json index 09bac771ad..5b30980831 100644 --- a/packages/modules/region/src/migrations/.snapshot-medusa-region.json +++ b/packages/modules/region/src/migrations/.snapshot-medusa-region.json @@ -1,7 +1,5 @@ { - "namespaces": [ - "public" - ], + "namespaces": ["public"], "name": "public", "tables": [ { @@ -33,6 +31,16 @@ "nullable": false, "mappedType": "text" }, + "automatic_taxes": { + "name": "automatic_taxes", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + }, "metadata": { "name": "metadata", "type": "jsonb", @@ -79,9 +87,7 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_region_deleted_at", "primary": false, @@ -89,9 +95,7 @@ }, { "keyName": "region_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -102,15 +106,6 @@ }, { "columns": { - "id": { - "name": "id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, "iso_2": { "name": "iso_2", "type": "text", @@ -171,9 +166,7 @@ "indexes": [ { "keyName": "region_country_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["iso_2"], "composite": false, "primary": true, "unique": true @@ -183,13 +176,9 @@ "foreignKeys": { "region_country_region_id_foreign": { "constraintName": "region_country_region_id_foreign", - "columnNames": [ - "region_id" - ], + "columnNames": ["region_id"], "localTableName": "public.region_country", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.region", "deleteRule": "set null", "updateRule": "cascade" diff --git a/packages/modules/region/src/migrations/RegionModuleSetup20240205173216.ts b/packages/modules/region/src/migrations/Migration20240205173216.ts similarity index 100% rename from packages/modules/region/src/migrations/RegionModuleSetup20240205173216.ts rename to packages/modules/region/src/migrations/Migration20240205173216.ts diff --git a/packages/modules/store/integration-tests/__fixtures__/index.ts b/packages/modules/store/integration-tests/__fixtures__/index.ts index fa59befca4..32bfacd168 100644 --- a/packages/modules/store/integration-tests/__fixtures__/index.ts +++ b/packages/modules/store/integration-tests/__fixtures__/index.ts @@ -2,7 +2,10 @@ import { StoreTypes } from "@medusajs/types" export const createStoreFixture: StoreTypes.CreateStoreDTO = { name: "Test store", - supported_currency_codes: ["usd", "eur"], + supported_currencies: [ + { currency_code: "usd" }, + { currency_code: "eur", is_default: true }, + ], default_sales_channel_id: "test-sales-channel", default_region_id: "test-region", metadata: { diff --git a/packages/modules/store/integration-tests/__tests__/store-module-service.spec.ts b/packages/modules/store/integration-tests/__tests__/store-module-service.spec.ts index a7f05d2f0e..4927f04303 100644 --- a/packages/modules/store/integration-tests/__tests__/store-module-service.spec.ts +++ b/packages/modules/store/integration-tests/__tests__/store-module-service.spec.ts @@ -16,7 +16,10 @@ moduleIntegrationTestRunner({ expect(store).toEqual( expect.objectContaining({ name: "Test store", - supported_currency_codes: expect.arrayContaining(["eur", "usd"]), + supported_currencies: expect.arrayContaining([ + expect.objectContaining({ currency_code: "eur" }), + expect.objectContaining({ currency_code: "usd" }), + ]), default_sales_channel_id: "test-sales-channel", default_region_id: "test-region", metadata: { @@ -25,14 +28,19 @@ moduleIntegrationTestRunner({ }) ) }) - }) - it("should fail to get created if default currency code is not in list of supported currency codes", async function () { - const err = await service - .createStores({ ...createStoreFixture, default_currency_code: "jpy" }) - .catch((err) => err.message) + it("should fail to get created if there is no default currency", async function () { + const err = await service + .createStores({ + ...createStoreFixture, + supported_currencies: [{ currency_code: "usd" }], + }) + .catch((err) => err.message) - expect(err).toEqual("Store does not have currency: jpy") + expect(err).toEqual( + "There should be a default currency set for the store" + ) + }) }) describe("upserting a store", () => { @@ -75,44 +83,46 @@ moduleIntegrationTestRunner({ expect(updatedStore.name).toEqual("Updated store") }) - it("should fail updating default currency code to an unsupported one", async function () { + it("should fail updating currencies without a default one", async function () { const createdStore = await service.createStores(createStoreFixture) const updateErr = await service .updateStores(createdStore.id, { - default_currency_code: "jpy", - }) - .catch((err) => err.message) - - expect(updateErr).toEqual("Store does not have currency: jpy") - }) - - it("should fail updating default currency code to an unsupported one if the supported currencies are also updated", async function () { - const createdStore = await service.createStores(createStoreFixture) - const updateErr = await service - .updateStores(createdStore.id, { - supported_currency_codes: ["mkd"], - default_currency_code: "jpy", - }) - .catch((err) => err.message) - - expect(updateErr).toEqual("Store does not have currency: jpy") - }) - - it("should fail updating supported currencies if one of them is used as a default one", async function () { - const createdStore = await service.createStores({ - ...createStoreFixture, - default_currency_code: "eur", - }) - const updateErr = await service - .updateStores(createdStore.id, { - supported_currency_codes: ["jpy"], + supported_currencies: [{ currency_code: "usd" }], }) .catch((err) => err.message) expect(updateErr).toEqual( - "You are not allowed to remove default currency from store currencies without replacing it as well" + "There should be a default currency set for the store" ) }) + + it("should fail updating currencies where a duplicate currency code exists", async function () { + const createdStore = await service.createStores(createStoreFixture) + const updateErr = await service + .updateStores(createdStore.id, { + supported_currencies: [ + { currency_code: "usd" }, + { currency_code: "usd" }, + ], + }) + .catch((err) => err.message) + + expect(updateErr).toEqual("Duplicate currency codes: usd") + }) + + it("should fail updating currencies where there is more than 1 default currency", async function () { + const createdStore = await service.createStores(createStoreFixture) + const updateErr = await service + .updateStores(createdStore.id, { + supported_currencies: [ + { currency_code: "usd", is_default: true }, + { currency_code: "eur", is_default: true }, + ], + }) + .catch((err) => err.message) + + expect(updateErr).toEqual("Only one default currency is allowed") + }) }) describe("deleting a store", () => { diff --git a/packages/modules/store/src/migrations/.snapshot-medusa-store.json b/packages/modules/store/src/migrations/.snapshot-medusa-store.json index 8dc9b84d92..fc1df79d3e 100644 --- a/packages/modules/store/src/migrations/.snapshot-medusa-store.json +++ b/packages/modules/store/src/migrations/.snapshot-medusa-store.json @@ -25,25 +25,6 @@ "default": "'Medusa Store'", "mappedType": "text" }, - "supported_currency_codes": { - "name": "supported_currency_codes", - "type": "text[]", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "default": "'{}'", - "mappedType": "array" - }, - "default_currency_code": { - "name": "default_currency_code", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, "default_sales_channel_id": { "name": "default_sales_channel_id", "type": "text", @@ -138,6 +119,118 @@ ], "checks": [], "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "currency_code": { + "name": "currency_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "store_id": { + "name": "store_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "store_currency", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_store_currency_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_currency_deleted_at\" ON \"store_currency\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "store_currency_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "store_currency_store_id_foreign": { + "constraintName": "store_currency_store_id_foreign", + "columnNames": [ + "store_id" + ], + "localTableName": "public.store_currency", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.store", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } } ] } diff --git a/packages/modules/store/src/migrations/Migration20240621145944.ts b/packages/modules/store/src/migrations/Migration20240621145944.ts new file mode 100644 index 0000000000..b898d0f1c6 --- /dev/null +++ b/packages/modules/store/src/migrations/Migration20240621145944.ts @@ -0,0 +1,21 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240621145944 extends Migration { + async up(): Promise { + this.addSql( + 'create table if not exists "store_currency" ("id" text not null, "currency_code" text not null, "is_default" boolean not null default false, "store_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "store_currency_pkey" primary key ("id"));' + ) + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_store_currency_deleted_at" ON "store_currency" (deleted_at) WHERE deleted_at IS NOT NULL;' + ) + this.addSql( + 'alter table if exists "store_currency" add constraint "store_currency_store_id_foreign" foreign key ("store_id") references "store" ("id") on update cascade on delete cascade;' + ) + this.addSql( + 'alter table if exists "store" drop column if exists "supported_currency_codes";' + ) + this.addSql( + 'alter table if exists "store" drop column if exists "default_currency_code";' + ) + } +} diff --git a/packages/modules/store/src/models/currency.ts b/packages/modules/store/src/models/currency.ts new file mode 100644 index 0000000000..f68b2c6f8f --- /dev/null +++ b/packages/modules/store/src/models/currency.ts @@ -0,0 +1,81 @@ +import { + DALUtils, + Searchable, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" + +import { + BeforeCreate, + Entity, + OnInit, + PrimaryKey, + Property, + Filter, + ManyToOne, +} from "@mikro-orm/core" +import Store from "./store" + +const StoreCurrencyDeletedAtIndex = createPsqlIndexStatementHelper({ + tableName: "store_currency", + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) + +@Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) +export default class StoreCurrency { + @PrimaryKey({ columnType: "text" }) + id: string + + @Searchable() + @Property({ columnType: "text" }) + currency_code: string + + @Property({ columnType: "boolean", default: false }) + is_default?: boolean + + @ManyToOne(() => Store, { + columnType: "text", + fieldName: "store_id", + mapToPk: true, + nullable: true, + onDelete: "cascade", + }) + store_id: string | null + + @ManyToOne(() => Store, { + persist: false, + nullable: true, + }) + store: Store | null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @StoreCurrencyDeletedAtIndex.MikroORMIndex() + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "stocur") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "stocur") + } +} diff --git a/packages/modules/store/src/models/index.ts b/packages/modules/store/src/models/index.ts index 958612afdf..bca1be4340 100644 --- a/packages/modules/store/src/models/index.ts +++ b/packages/modules/store/src/models/index.ts @@ -1 +1,2 @@ export { default as Store } from "./store" +export { default as StoreCurrency } from "./currency" diff --git a/packages/modules/store/src/models/store.ts b/packages/modules/store/src/models/store.ts index 752a654105..ba6e1aa996 100644 --- a/packages/modules/store/src/models/store.ts +++ b/packages/modules/store/src/models/store.ts @@ -15,7 +15,11 @@ import { Property, Filter, OptionalProps, + OneToMany, + Collection, + Cascade, } from "@mikro-orm/core" +import StoreCurrency from "./currency" type StoreOptionalProps = DAL.SoftDeletableEntityDateColumns @@ -37,11 +41,10 @@ export default class Store { @Property({ columnType: "text", default: "Medusa Store" }) name: string - @Property({ type: "array", default: "{}" }) - supported_currency_codes: string[] = [] - - @Property({ columnType: "text", nullable: true }) - default_currency_code: string | null = null + @OneToMany(() => StoreCurrency, (o) => o.store, { + cascade: [Cascade.PERSIST, "soft-remove"] as any, + }) + supported_currencies = new Collection(this) @Property({ columnType: "text", nullable: true }) default_sales_channel_id: string | null = null diff --git a/packages/modules/store/src/services/store-module-service.ts b/packages/modules/store/src/services/store-module-service.ts index 0c83c1c79a..61a3eb5174 100644 --- a/packages/modules/store/src/services/store-module-service.ts +++ b/packages/modules/store/src/services/store-module-service.ts @@ -8,6 +8,7 @@ import { StoreTypes, } from "@medusajs/types" import { + getDuplicates, InjectManager, InjectTransactionManager, isString, @@ -18,7 +19,7 @@ import { removeUndefined, } from "@medusajs/utils" -import { Store } from "@models" +import { Store, StoreCurrency } from "@models" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { UpdateStoreInput } from "@types" @@ -30,7 +31,8 @@ type InjectedDependencies = { export default class StoreModuleService extends MedusaService<{ Store: { dto: StoreTypes.StoreDTO } - }>({ Store }, entityNameToLinkableKeysMap) + StoreCurrency: { dto: StoreTypes.StoreCurrencyDTO } + }>({ Store, StoreCurrency }, entityNameToLinkableKeysMap) implements IStoreModuleService { protected baseRepository_: DAL.RepositoryService @@ -81,7 +83,13 @@ export default class StoreModuleService let normalizedInput = StoreModuleService.normalizeInput(data) StoreModuleService.validateCreateRequest(normalizedInput) - return await this.storeService_.create(normalizedInput, sharedContext) + return ( + await this.storeService_.upsertWithReplace( + normalizedInput, + { relations: ["supported_currencies"] }, + sharedContext + ) + ).entities } async upsertStores( @@ -168,8 +176,15 @@ export default class StoreModuleService @MedusaContext() sharedContext: Context = {} ): Promise { const normalizedInput = StoreModuleService.normalizeInput(data) - await this.validateUpdateRequest(normalizedInput) - return await this.storeService_.update(normalizedInput, sharedContext) + StoreModuleService.validateUpdateRequest(normalizedInput) + + return ( + await this.storeService_.upsertWithReplace( + normalizedInput, + { relations: ["supported_currencies"] }, + sharedContext + ) + ).entities } private static normalizeInput( @@ -178,6 +193,10 @@ export default class StoreModuleService return stores.map((store) => removeUndefined({ ...store, + supported_currencies: store.supported_currencies?.map((c) => ({ + ...c, + currency_code: c.currency_code.toLowerCase(), + })), name: store.name?.trim(), }) ) @@ -185,77 +204,42 @@ export default class StoreModuleService private static validateCreateRequest(stores: StoreTypes.CreateStoreDTO[]) { for (const store of stores) { - // If we are setting the default currency code on creating, make sure it is supported - if (store.default_currency_code) { - if ( - !store.supported_currency_codes?.includes( - store.default_currency_code ?? "" - ) - ) { + if (store.supported_currencies?.length) { + const duplicates = getDuplicates( + store.supported_currencies?.map((c) => c.currency_code) + ) + + if (duplicates.length) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `Store does not have currency: ${store.default_currency_code}` + `Duplicate currency codes: ${duplicates.join(", ")}` + ) + } + + let seenDefault = false + store.supported_currencies?.forEach((c) => { + if (c.is_default) { + if (seenDefault) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Only one default currency is allowed` + ) + } + seenDefault = true + } + }) + + if (!seenDefault) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `There should be a default currency set for the store` ) } } } } - private async validateUpdateRequest(stores: UpdateStoreInput[]) { - const dbStores = await this.storeService_.list( - { id: stores.map((s) => s.id) }, - { take: null } - ) - - const dbStoresMap = new Map( - dbStores.map((dbStore) => [dbStore.id, dbStore]) - ) - - for (const store of stores) { - const dbStore = dbStoresMap.get(store.id) - - // If it is updating both the supported currency codes and the default one, look in that list - if (store.supported_currency_codes && store.default_currency_code) { - if ( - !store.supported_currency_codes.includes( - store.default_currency_code ?? "" - ) - ) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Store does not have currency: ${store.default_currency_code}` - ) - } - return - } - - // If it is updating only the default currency code, look in the db store - if (store.default_currency_code) { - if ( - !dbStore?.supported_currency_codes?.includes( - store.default_currency_code - ) - ) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Store does not have currency: ${store.default_currency_code}` - ) - } - } - - // If it is updating only the supported currency codes, make sure one of them is not set as a default one - if (store.supported_currency_codes) { - if ( - !store.supported_currency_codes.includes( - dbStore?.default_currency_code ?? "" - ) - ) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "You are not allowed to remove default currency from store currencies without replacing it as well" - ) - } - } - } + private static validateUpdateRequest(stores: UpdateStoreInput[]) { + StoreModuleService.validateCreateRequest(stores) } }