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) } }