Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .changeset/buyget-target-in-buy-rules-order-dependence.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
"@medusajs/promotion": patch
---

fix(promotion): apply buy-get promotions independently of line item order

A `buyget` promotion whose target product is also listed in its own `buy_rules` was applied or not depending purely on the order of line items in the cart, and when it was not applied nothing surfaced — no error, no adjustment, the cart silently stayed at full price.

Two things caused it. `sortByPrice` in `buy-get.ts` never returned `0`, so for equally priced items it claimed a strict ordering that does not exist and left the result up to the sort implementation. The buy-side selection then greedily reserved the first `buy_rules_min_quantity` eligible items and subtracted them from the target's available quantity, so whenever the target item was the one reserved, the promotion was dropped even though a valid assignment existed.

The comparators now return `0` for equal prices (also in `sort-by-price.ts`, where `sortLineItemByPriceAscending` and `sortShippingLineByPriceAscending` had the same problem), and the buy side now prefers items that cannot serve as targets before falling back to target-eligible ones. Buy X get X on a single product is unaffected.
---
"@medusajs/promotion": patch
---
fix(promotion): apply buy-get promotions independently of line item order
A `buyget` promotion whose target product is also listed in its own `buy_rules` was applied or not depending purely on the order of line items in the cart, and when it was not applied nothing surfaced — no error, no adjustment, the cart silently stayed at full price.
Two things caused it. `sortByPrice` in `buy-get.ts` never returned `0`, so for equally priced items it claimed a strict ordering that does not exist and left the result up to the sort implementation. The buy-side selection then greedily reserved the first `buy_rules_min_quantity` eligible items and subtracted them from the target's available quantity, so whenever the target item was the one reserved, the promotion was dropped even though a valid assignment existed.
The comparators now return `0` for equal prices (also in `sort-by-price.ts`, where `sortLineItemByPriceAscending` and `sortShippingLineByPriceAscending` had the same problem), and the buy side now prefers items that cannot serve as targets before falling back to target-eligible ones. Buy X get X on a single product is unaffected.
10 changes: 5 additions & 5 deletions .changeset/normalize-locale-script-subtag.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@medusajs/utils": patch
---

fix(utils): title-case script subtags in two-segment locales
---
"@medusajs/utils": patch
---
fix(utils): title-case script subtags in two-segment locales
5 changes: 5 additions & 0 deletions .changeset/reason-list-row-href-edit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---

fix(dashboard): point refund and return reason row links at their edit routes
82 changes: 41 additions & 41 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
.env
node_modules
*yarn-error.log
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

packages/**/.yarn/*
integration-tests/**/.yarn/*
www/**/.yarn/*

packages/**/plugins/**/.medusa/

.DS_Store

.eslintcache

.idea
.turbo
build/**
dist/**
**/dist
**/stats
.favorites.json
.vscode

**/tsconfig.tsbuildinfo

# Cache File
/packages/**/.cache

.cursorignore
**/.medusa

# Generated by `storybook build` and `vitest --coverage`
**/storybook-static
**/coverage
node_modules
*yarn-error.log
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
packages/**/.yarn/*
integration-tests/**/.yarn/*
www/**/.yarn/*
packages/**/plugins/**/.medusa/
.DS_Store
.eslintcache
.idea
.turbo
build/**
dist/**
**/dist
**/stats
.favorites.json
.vscode
**/tsconfig.tsbuildinfo
# Cache File
/packages/**/.cache
.cursorignore
**/.medusa
# Generated by `storybook build` and `vitest --coverage`
**/storybook-static
**/coverage
config.bat
Original file line number Diff line number Diff line change
@@ -1,65 +1,65 @@
import { HttpTypes } from "@medusajs/types"
import { TFunction } from "i18next"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useRefundReasons } from "../../../../../hooks/api"
import {
createTableAdapter,
TableAdapter,
} from "../../../../../lib/table/table-adapters"
import { RefundReasonListTableActions } from "./refund-reason-list-table-actions"

export function createRefundReasonTableAdapter({
t,
}: {
t: TFunction<"translation", undefined>
}): TableAdapter<HttpTypes.AdminRefundReason> {
return createTableAdapter<HttpTypes.AdminRefundReason>({
entity: "refund-reasons",
queryPrefix: "rfr",
pageSize: 20,
emptyState: {
empty: { heading: t("general.noRecordsMessage") },
filtered: {
heading: t("general.noRecordsMessage"),
description: t("general.noRecordsMessageFiltered"),
},
},
useData: (fields, params) => {
const { refund_reasons, count, isError, error, isLoading } =
useRefundReasons(
{ fields, ...params },
{
placeholderData: (previousData, previousQuery: any) => {
const prevFields =
previousQuery?.[previousQuery?.length - 1]?.query?.fields
if (prevFields && prevFields !== fields) {
return undefined
}
return previousData
},
}
)
return { data: refund_reasons, count, isLoading, isError, error }
},
getRowHref: (row) => `/settings/refund-reasons/${row.id}`,
renderRowActions: (row) => (
<RefundReasonListTableActions refundReason={row} />
),
transformColumns: (columns) => {
const ALLOWED_FILTERS = ["id", "created_at", "updated_at", "deleted_at"]
return columns.map((column) => ({
...column,
filter: !ALLOWED_FILTERS.includes(column.field)
? { ...column.filter, enabled: false }
: column.filter,
}))
},
})
}

// eslint-disable-next-line max-len
export function useRefundReasonTableAdapter(): TableAdapter<HttpTypes.AdminRefundReason> {
const { t } = useTranslation()
return useMemo(() => createRefundReasonTableAdapter({ t }), [t])
}
import { HttpTypes } from "@medusajs/types"
import { TFunction } from "i18next"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useRefundReasons } from "../../../../../hooks/api"
import {
createTableAdapter,
TableAdapter,
} from "../../../../../lib/table/table-adapters"
import { RefundReasonListTableActions } from "./refund-reason-list-table-actions"
export function createRefundReasonTableAdapter({
t,
}: {
t: TFunction<"translation", undefined>
}): TableAdapter<HttpTypes.AdminRefundReason> {
return createTableAdapter<HttpTypes.AdminRefundReason>({
entity: "refund-reasons",
queryPrefix: "rfr",
pageSize: 20,
emptyState: {
empty: { heading: t("general.noRecordsMessage") },
filtered: {
heading: t("general.noRecordsMessage"),
description: t("general.noRecordsMessageFiltered"),
},
},
useData: (fields, params) => {
const { refund_reasons, count, isError, error, isLoading } =
useRefundReasons(
{ fields, ...params },
{
placeholderData: (previousData, previousQuery: any) => {
const prevFields =
previousQuery?.[previousQuery?.length - 1]?.query?.fields
if (prevFields && prevFields !== fields) {
return undefined
}
return previousData
},
}
)
return { data: refund_reasons, count, isLoading, isError, error }
},
getRowHref: (row) => `/settings/refund-reasons/${row.id}/edit`,
renderRowActions: (row) => (
<RefundReasonListTableActions refundReason={row} />
),
transformColumns: (columns) => {
const ALLOWED_FILTERS = ["id", "created_at", "updated_at", "deleted_at"]
return columns.map((column) => ({
...column,
filter: !ALLOWED_FILTERS.includes(column.field)
? { ...column.filter, enabled: false }
: column.filter,
}))
},
})
}
// eslint-disable-next-line max-len
export function useRefundReasonTableAdapter(): TableAdapter<HttpTypes.AdminRefundReason> {
const { t } = useTranslation()
return useMemo(() => createRefundReasonTableAdapter({ t }), [t])
}
Loading
Loading