**What**
- Adds `metadata` to the default response for `/admin/collection` endpoints.
- Fixes tests to expect correct shape and not use objectContaining.
Fixes CC-551
**What**
- Fixes an issue where the admin dashboard would send region prices for deleted regions.
- Also, includes the implementation for `GET /admin/shipping-options/:id` and its corresponding SDK function.
**Why**
- When a region price for a deleted region was sent to the backend it would result in the insert hitting a not null constraint on the currency_code for prices. To avoid this the dashboard should not send region prices for deleted regions.
**Additional context**
- Prices for deleted regions should ideally not be returned when fetching shipping option prices. However, we don't yet have a mechanism for cleaning up region prices after a region is deleted.
Fixes CC-540
**What**
- Adds a step to `updateShippingOptionsWorkflow` and `createShippingOptionsWorkflow` that validates if the region prices being updated have corresponding regions configured.
**Why**
- Previously, if you tried to send a region price update for a region that had been deleted the backend would throw an error when attempting to insert the region price. The error comes from a not-null constraint in the db, but it is better to validate that the regions we are trying to create prices for exist.
Fixes CC-542
**What**
- Fetches the stock location's details when creating a fulfillment and return fulfillment.
- Passes the data to the fulfillment module, which in turn passes it to the fulfillment provider.
**Why**
- When creating a fulfillment in a multi-location setup the fulfillment provider will need to know where the package is being sent from (so the shipping service can pick it up).
- Previously, we didn't pass anything but the location id to the fulfillment provider. Because the fulfillment provider can't have a dependency on the stock location module this was not sufficient.
- This change ensures there is enough data passed to the fulfillment provider to build integrations properly.
* fix: skip module loading if mod: false
* fix: don't include disabled modules in config
* add test
* fix: revert changes to module loading in medusaapp
**What**
- Fixes tsconfig to ensure a declaration file is included in the build.
**Why**
- Ensure tsserver can understand things from medusa-test-utils
- Get rid of "Could not find a declaration file for module" warning in editors.
**What**
- Adds support for starting a Medusa process with a worker mode.
- The worker modes supported are "shared", "worker", "server"
- In "worker" mode, API routes are not registered and modules that need to run workers (e.g., event bus redis) can use the flag to conditionally start workers.
- In "server" mode, API routes are registered and workers are not started.
- In "shared" mode, API routes are registered and workers are started. This is great for development.
**What**
- Runs loadMedusaV2 when v2 flag is enabled.
- V2 loader loads only project-level framework features. E.g., project APIs, subscribers, jobs, and workflows.
- No plugin support yet.
**What**
Adds endpoints to manage tax rules on a tax rate:
- Create a tax rule: POST /admin/tax-rates/:id/rules
- Delete a tax rule: DELETE /admin/tax-rates/:id/rules/:rule_id
- Replace tax rules: POST /admin/tax-rates/:id -- with { rules: [...] } in body.
### Noteworthy things I bumped into
**Updating nested relationships**
A TaxRate can have multiple TaxRules and in this PR we enable users to replace all TaxRules associated with a TaxRate in one operation. If working with the module directly this can be done with:
```javascript
taxModuleService.update(rateId, { rules: [{ ... }] })
```
Internally in the `update` function the TaxModule first soft deletes any TaxRules that exist on the TaxRate and then creates new TaxRules for the passed rules ([see test](https://github.com/medusajs/medusa/pull/6557/files#diff-cdcbab80ac7928b80648088ec57a3ab09dddd4409d6afce034f2caff08ee022bR78)).
A challenge arises when doing this in a compensatable way in a workflow. To see this imagine the following:
1. `updateTaxRatesWorkflow` gets the current data for the tax rates to update. This includes the tax rates' rules.
2. `updateTaxRatesWorkflow` calls `taxModuleService.update` with new rules.
3. Internally, the tax module deletes the rules in 1. and creates new rules.
4. Imagine an error happens in a following step and the workflow has to compensate.
5. The workflow uses the data from 1. and calls upsert. The tax module may correctly update the previous tax rules so they are no longer soft deleted. However, upsert (at least not by default) doesn't delete the new rules that were created in 2.
As illustrated by 5. compensating the update is not pretty. To get around this I instead opted to let the workflow handle setting the rules for a rate that makes the compensation more straightforward to handle. [See workflow here](https://github.com/medusajs/medusa/pull/6557/files#diff-ff19e1f2fa32289aefff90d33c05c154f9605a3c5da6a62683071a1fcaedfd7bR89).
**Using nested workflows**
Initially, I wanted to use the `setTaxRateRulesWorkflow` within the `updateTaxRatesWorkflow`. And this worked great for the invoke phase. However, when I needed to compensate the update workflow (and hence also had to compensate the set rules workflow), I found that the workflow engine no longer had the set rules transaction in memory and therefore could not roll it back. ([This is where I try to rollback](https://github.com/medusajs/medusa/pull/6557/files#diff-ff19e1f2fa32289aefff90d33c05c154f9605a3c5da6a62683071a1fcaedfd7bR62), but the transaction id can't be found).
I therefore opted to copy the steps from the set tax rate rules workflow into the update tax rates workflow; however, once we figure out a good way to ensure we can compensate nested workflows we should move to the nested workflow instead.
This also made me realize that the current implementation of workflows that use `refreshCartPromotions` may create inconsistencies in case of failures (cc: @riqwan).
**What**
- Ensures that country and province codes are always stored as lower case.
- Ensures that calculation context normalizes input before getting region rates.
**What**
- Selects the correct tax line for an item given a calculation context.
**For later PR**
- Consider optimizations. Some thoughts:
- Even with global sales the number of rates in the DB is not likely to grow beyond ~1000.
- Can large orders with hundreds of items optimize somehow?
- Does it make sense to write a custom SQL query to do this?
- Support combined rate.
**Test cases covered**
The selection of tax rates take the following priority:
1. specific product rules - province
2. specific product type rules - province
3. default province rules
4. specific product rules - country
5. specific product type rules - country
6. default country rules
There are test cases for each of them under the following data seed structure:
### **US**
- **Default Rate**: 2%
- **Sub-Regions**
- CA
- Default Rate: 5%
- Overrides
- Reduced rate (for 3 product ids): 3%
- Reduced rate (for product type): 1%
- NY
- Default rate: 6%
- FL
- Default rate: 4%
- **Overrides**
- None
### **Denmark**
- **Default rate:** 25%
- **Sub-Regions**
- None
- **Overrides**
- None
### **Germany**
- **Default Rate:** 19%
- **Sub-Regions**
- None
- **Overrides:**
- Reduced Rate (for product type) - 7%
### **Canada**
- **Default rate**: 5%
- **Sub-Regions**
- QC
- Default rate: 2%
- Overrides:
- Reduced rate (for same product type as country reduced rate): 1%
- BC
- Default rate: 2%
- **Overrides**
- Reduced rate (for product id) - 3%
- Reduced rate (for product type) - 3.5%
**What**
- Adds Tax rules to allow overrides of tax rates for certain products, product types or shipping options.
**Punted to future PR**
- Currently, the creation methods only support bulk operations. A later PR will include support for singular operations, too.
- It should be possible to add products, types, and shipping options to a tax rate after creating it. Add, remove, update will come in a later PR.
**What**
- Adds a TaxRegion entity.
**For context: High-level design of the Tax module**
- A TaxRegion scopes tax rates to a geographical place.
- You can define tax regions on two levels: country-level, province-level (this corresponds to state in US contexts)
- Each Tax Region can have a default Tax Rate.
- [not yet done] - Each Tax Region can also have granularly defined tax rates for different products and shipping rates. For example, California can have a base rate for default products, but a reduced rate for groceries.
- Tax Rates specify if they can be combined with other rates - it's always the lowest level rate that wins.
The above allows a merchant to define their tax settings along the lines of this:
- Denmark (Region)
- Default rate: 25% (TaxRate)
- Germany (Region)
- Default rate: 19% (TaxRate)
- Reduced rate (books): 9% (TaxRate w. rule)
- United States (Region)
- Default rate: 0% (TaxRate)
- California: (Region)
- Default rate: 7.25% (TaxRate)
- Arkansas: (Region)
- Default rate: 6.5%
- Reduced rate (groceries): 0.125% (TaxRate w. rule)
The TaxModule can then receive a list of products and the shipping address to determine what tax rates apply to the line items.
**What**
- Catches unique constraints on customer_id, is_default_billing/is_default_shipping and reformats
- Adds an step to create and update of addresses that unsets the previous default shipping/billing address if necessary.
- This creates a behavior in the API where you can always set an address to be default and it will automatically unset the previous one for you.
- GET /customers/me/addresses
- POST /customers/me/addresses
- GET /customers/me/addresses/:address_id
- POST /customers/me/addresses/:address_id
- DELETE /customers/me/addresses/:address_id
**What**
- GET /admin/customer-groups/:id/customers
- POST /admin/customer-groups/:id/customers/batch
- POST /admin/customer-groups/:id/customers/remove
Workflows
**What**
- GET /admin/customers/:id/addresses
- POST /admin/customers/:id/addresses
- POST /admin/customers/:id/addresses/:address_id
- DELETE /admin/customers/:id/addresses/:address_id
**What**
```
POST /admin/customer-groups
POST /admin/customer-groups/:id
GET /admin/customer-groups/:id
DELETE /admin/customer-groups/:id
```
- Workflows
**What**
- Provide a more helpful error message if a user adds a file in `src/services` that doesn't export a service class.
**Why**
If you forget to `export default MyClass` in a custom service you can end up with this error:

It's almost impossible to know how to recover from this which can be an issue for new users. The new error message informs the user that there is a missing class export and shows which file the error is related to.
**What**
- adds methods to create update list customer addresses
- removes default_shipping_id and billing id from customer record and moves them to address (better normalization)
**What**
Methods for:
- Bulk create customers
- Bulk and single create of customer groups
- Assigning customer <> group relationships
- listing of customers and customer groups
++ Uses new repository loading mechanism
**What**
- ProductService.create calls ProductService.retrieve before returning. This fix ensures that the manager created in the atomicPhase is used when ProductService.retrieve is called.
**Why**
- Without the explicit use of the same manager in the retrieve call, race conditions easily occur if you have concurrent ProductService.create calls. The code below can reproduce the scenario:
```javascript
const productData = [
{
barcode: `1234233423-${Math.random() * 100}`,
external_id: `1234233423-${Math.random() * 100}`,
description: "super cool product",
discountable: true,
hs_code: "1234213",
origin_country: "DK",
title: `Super cool product ${Math.random() * 100}`,
type: { value: "Eyewear" },
sales_channels: [{ id: sc.id }],
},
{
barcode: `1234233423-${Math.random() * 100}`,
external_id: `random-external-id-${Math.random() * 100}`,
description: "super cool product",
discountable: true,
hs_code: "1234213",
origin_country: "DK",
title: `Super cool product ${Math.random() * 100}`,
type: { value: "Eyewear" },
sales_channels: [{ id: sc.id }],
},
];
const result = await Promise.all(
productData.map(async (product) => productService.create(product))
);
```
**Explaination**
What happens is the following:
- Request 1 calls `ProductService.create`. This in turn calls `atomicPhase` which sets the `ProductService.transactionManager_ = txForReqOne`
- Request 1 creates the product in the DB with `txForReqOne`.
- Request 2 calls `ProductService.create`. This in turn calls `atomicPhase` which sets the `ProductService.transactionManager_ = txForReqTwo`
- Request 1 reaches the end of `ProductService.create` where `this.retrieve` is called. Because the ProductService is a singleton the retrieve call will attempt to use `txForReqTwo` to fetch the product.
- **Error**: since `txForReqTwo` can't read the data of `txForReqOne`, the product is not found.
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
Fixes#6068.
**What**
- Uses new Subscriber API.
- Adds support for typescript files in Segment plugin.
- Adds a peerDependency on @medusajs/medusa for the version that introduced new Subscriber API.
**Why**
- We have some workflow-like flows in @medusajs/medusa. These should be moved over to the workflows package.
- Inventory Items <> Variant currently assume a 1-1 mapping. There should be support for a many-to-many mapping.
**What**
- PR introduces a feature flag for supporting many-to-many mappings for inventory and variants.
- Deletes legacy transaction handler in @medusajs/medusa.
- Adjusts existing createInventoryItems handler to remove dependency on variant data.
**Unkowns**
~~1. Couldn't find an existing test for the CreateProduct workflow. It should be tested that this still works as expected.~~
2. Have removed transaction managers as we should move to handling consistency through orchestration tooling. Are we ready for that?
**What**
Allow DraftOrders to be created with an IdempotencyKey.
Note this doesn't implement idempotency for the DraftOrder create endpoint but allows the service layer to ingest the key and store it in the database. This is a preliminary step to being able to support an idempotent API request.
**What**
BP plugin was recording total shipping price based on the shipping_method.price; however, this value may be with or without taxes depending on the tax inclusivity setting. This change ensures that the shipping price is calculated correctly.
**What**
The existing totals calculations are extremely heavy and perform an enormous amount of duplicate work. The changes here remove large parts of the overhead and improves response times for cart endpoints up to 30x.
**What**
Since the release of the Tax API the line item totals calculations on orders with gift cards have been wrong. To understand the bug consider the below order:
Region:
- tax_rate: 25%
- gift_cards_taxable: true
Order:
- applied gift card: 1000
- items:
- A: unit_price: 1000
- B: unit_price: 500
- Subtotal: 1500
**Previous calculation method**
1. Determine how much of the gift card is used for each item using `item_total / subtotal * gift_card_amount`:
- Item A: 1000/1500 * 1000 = 666.67
- Item B: 500/1500 * 1000 = 333.33
2. Calculate line item totals including taxes using `(unit_price - gift_card) * (1 + tax_rate)`
- Item A: 1000 - 666.67 = 333.33; vat amount -> 83.33
- Item B: 500 - 333.33 = 166.67; vat amount -> 41.67
3. Add up the line item totals: order subtotal = 500; vat amount = 125; total = 625
This is all correct at the totals level; but at the line item level we should still use the "original prices" i.e. the line item total for item a should be (1000 * 1.25) = 1250 with a tax amount of 250.
**New calculation method**
1. Use default totals calculations
- Item A: subtotal: 1000, tax_total: 250, total: 1250
- Item B: subtotal: 500, tax_total: 125, total: 625
2. Add up the line item totals: subtotal: 1500, tax_total: 375, total: 1875
3. Reduce total with gift card: subtotal: 1500 - 1000 = 500, tax_total: 375 - 250 = 125, total = 625
Totals can now be forwarded correctly to accounting plugins.
Fixes CORE-310.
**Usage**
**Create a new feature flag**
```
$ medusa-dev ff create [name of flag] -d [description of what flag is for]
```
Will put a new file in `packages/medusa/src/loaders/feature-flags/[kebab-cased-flag-name].ts` and fill out the details.
**List feature flags**
```
$ medusa-dev ff list
```
Note: your Medusa repo must be built for the flags to show up
**Delete a feature flag**
```
$ medusa-dev ff delete [name of flag]
```
Will delete a file at `packages/medusa/src/loaders/feature-flags/[kebab-cased-flag-name].ts` if it exists.