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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -248,10 +248,13 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
||||
|
||||
<Select.Content>
|
||||
{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) => (
|
||||
<Select.Item
|
||||
|
||||
@@ -22,7 +22,7 @@ export const CreateShippingOptionsPricesForm = ({
|
||||
} = useStore()
|
||||
|
||||
const currencies = useMemo(
|
||||
() => store?.supported_currency_codes || [],
|
||||
() => store?.supported_currencies?.map((c) => c.currency_code) || [],
|
||||
[store]
|
||||
)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export function EditShippingOptionsPricingForm({
|
||||
} = useStore()
|
||||
|
||||
const currencies = useMemo(
|
||||
() => store?.supported_currency_codes || [],
|
||||
() => store?.supported_currencies?.map((c) => c.currency_code) || [],
|
||||
[store]
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ({
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="includes_tax"
|
||||
name="automatic_taxes"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<Form.Label>
|
||||
{t("fields.taxInclusivePricing")}
|
||||
{t("fields.automaticTaxes")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
@@ -295,7 +295,7 @@ export const CreateRegionForm = ({
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
{t("regions.taxInclusiveHint")}
|
||||
{t("regions.automaticTaxesHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</div>
|
||||
@@ -303,6 +303,7 @@ export const CreateRegionForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="bg-ui-border-base h-px w-full" />
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 = () => {
|
||||
<CurrencyActions
|
||||
storeId={storeId}
|
||||
currency={row.original}
|
||||
currencyCodes={currencyCodes}
|
||||
supportedCurrencies={supportedCurrencies}
|
||||
defaultCurrencyCode={defaultCurrencyCode}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
@@ -51,13 +53,13 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.defaultCurrency")}
|
||||
</Text>
|
||||
{store.default_currency ? (
|
||||
{defaultCurrency ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge size="2xsmall">
|
||||
{store.default_currency.code.toUpperCase()}
|
||||
{defaultCurrency.currency_code.toUpperCase()}
|
||||
</Badge>
|
||||
<Text size="small" leading="compact">
|
||||
{store.default_currency.name}
|
||||
{defaultCurrency.currency.name}
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -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) => {
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{store.supported_currency_codes.map((code) => (
|
||||
<Select.Item key={code} value={code}>
|
||||
{code.toUpperCase()}
|
||||
{store.supported_currencies?.map((currency) => (
|
||||
<Select.Item
|
||||
key={currency.currency_code}
|
||||
value={currency.currency_code}
|
||||
>
|
||||
{currency.currency_code.toUpperCase()}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<string, unknown>
|
||||
|
||||
/**
|
||||
* Whether prices include taxes in the region.
|
||||
*/
|
||||
includes_tax?: boolean
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./common"
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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.
|
||||
|
||||
@@ -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<StoreDTO>
|
||||
@@ -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",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,8 +21,14 @@ export const AdminGetStoresParams = createFindParams({
|
||||
export type AdminUpdateStoreType = z.infer<typeof AdminUpdateStore>
|
||||
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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
@@ -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"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -16,7 +16,10 @@ moduleIntegrationTestRunner<IStoreModuleService>({
|
||||
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<IStoreModuleService>({
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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<IStoreModuleService>({
|
||||
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", () => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240621145944 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
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";'
|
||||
)
|
||||
}
|
||||
}
|
||||
81
packages/modules/store/src/models/currency.ts
Normal file
81
packages/modules/store/src/models/currency.ts
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { default as Store } from "./store"
|
||||
export { default as StoreCurrency } from "./currency"
|
||||
|
||||
@@ -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<StoreCurrency>(this)
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
default_sales_channel_id: string | null = null
|
||||
|
||||
@@ -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<Store[]> {
|
||||
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<T extends StoreTypes.UpdateStoreDTO>(
|
||||
@@ -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<string, Store>(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user