feat:swap/claim on claim and claim on swap (#424)

This commit is contained in:
Sebastian Mateos Nicolajsen
2021-10-14 18:21:38 +02:00
committed by GitHub
parent b6efa6f471
commit fbd08e0feb
18 changed files with 1745 additions and 1678 deletions

View File

@@ -41,26 +41,26 @@ Medusa is an open-source headless commerce engine that enables developers to cre
## 🚀 Quickstart
1. **Install Medusa CLI**
```bash
npm install -g @medusajs/medusa-cli
```
```bash
npm install -g @medusajs/medusa-cli
```
2. **Create a new Medusa project**
```
medusa new my-medusa-store --seed
```
```
medusa new my-medusa-store --seed
```
3. **Start your Medusa engine**
```bash
medusa develop
```
```bash
medusa develop
```
4. **Use the API**
```bash
curl localhost:9000/store/products | python -m json.tool
```
```bash
curl localhost:9000/store/products | python -m json.tool
```
After these four steps and only a couple of minutes, you now have a complete commerce engine running locally. You may now explore [the documentation](https://docs.medusa-commerce.com/api) to learn how to interact with the Medusa API. You may also add [plugins](https://github.com/medusajs/medusa/tree/master/packages) to your Medusa store by specifying them in your `medusa-config.js` file.
## 🛒 Setting up a storefront for your Medusa project
Medusa is a headless commerce engine which means that it can be used for any type of digital commerce experience - you may use it as the backend for an app, a voice application, social commerce experiences or a traditional e-commerce website, you may even want to integrate Medusa into your own software to enable commerce functionality. All of these are use cases that Medusa supports - to learn more read the documentation or reach out.
To provide a quick way to get you started with a storefront install one of our traditional e-commerce starters:
@@ -78,7 +78,9 @@ To provide a quick way to get you started with a storefront install one of our t
With your starter and your Medusa store running you can open http://localhost:8000 (for Gatsby) or http://localhost:3000 (for Nextjs) in your browser and view the products in your store, build a cart, add shipping details and pay and complete an order.
## ⭐️ Features
Medusa comes with a set of building blocks that allow you to create amazing digital commerce experiences, below is a list of some of the features that Medusa come with out of the box:
- **Headless**: Medusa is a highly customizable commerce API which means that you may use any presentation layer such as a website, app, chatbots, etc.
- **Regions** allow you to specify currencies, payment providers, shipping providers, tax rates and more for one or more countries for truly international sales.
- **Orders** come with all the functionality necessary to perform powerful customer service operations with ease.
@@ -89,13 +91,14 @@ Medusa comes with a set of building blocks that allow you to create amazing digi
- **Returns** allow customers to send back products and can be configured to function in a 100% automated flow through accounting and payment plugins.
- **Fulfillment API** makes it easy to integrate with any fulfillment provider by creating fulfillment plugins, check the `/packages` directory for a full list of plugins.
- **Payments API** makes it easy to integrate with any payment provider by creating payment plugins, we already support Stripe, Paypal and Klarna.
- **Notification API** allow integrations with email providers, chatbots, Slack channels, etc.
- **Customer Login** to give customers a way of managing their data, viewing their orders and saving payment details.
- **Notification API** allow integrations with email providers, chatbots, Slack channels, etc.
- **Customer Login** to give customers a way of managing their data, viewing their orders and saving payment details.
- **Shipping Options & Profiles** enable powerful rules for free shipping limits, multiple fulfillment methods and more.
- **Medusa's Plugin Architecture** makes it intuitive and easy to manage your integrations, switch providers and grow with ease.
- **Customization** is supported for those special use cases that all the other e-commerce platforms can't accommodate.
## Database support
In production Medusa requires Postgres and Redis, but SQLite is supported for development and testing purposes. If you plan on using Medusa for a project it is recommended that you install Postgres and Redis on your dev machine.
- [Install PostgreSQL](https://www.postgresql.org/download/)

View File

@@ -95,6 +95,7 @@ Update `scripts` to the following:
Finally, deploy your Redis and Postgres followed by your Medusa application.
#### Deploy databases
In your environment overview in Qovery, deploy your databases one after the other. Only when these are deployed, proceed to next step.
#### Push changes to your repository

View File

@@ -6,20 +6,22 @@ title: Making your store more powerful with Contentful
In [part 1](https://docs.medusa-commerce.com/how-to/headless-ecommerce-store-with-gatsby-contentful-medusa/) of this series you have set up [Medusa](https://medusa-commerce.com) with Contentful as your CMS system and added a Gatsby storefront. In this part you will get a further introduction to Contentful and learn how [`medusa-plugin-contentful`](https://github.com/medusajs/medusa/tree/master/packages/medusa-plugin-contentful) can be leveraged to make your store more powerful. Apart from a front page, product pages and a checkout flow, most ecommerce stores also need miscalleneous pages like About and Contact pages. In this guide you will add a Rich Text content module to your Contentful space so that you can make this pages cool. You will also see how the content modules can be used to give your product pages more life.
What you will do in this guide:
- Add a rich text content module
- Add rich text to your `/about` page
- Add a "Related Products" section to your product page
Topics covered:
- Contentful Migrations
- Product enrichment
## Creating a rich text content module
In this guide you will make use of [Contentful Migrations](https://github.com/contentful/contentful-migration) to keep a versioned controlled record of how your Content evolves over time. The Contentful app allows you to create content models straight from their dashboard, however, when using the migrations tool you will be able to 1) quickly replicate your Contentful space and 2) incorporate migrations as part of a CI/CD pipeline. [You can read more about how to use CMS as Code here](https://www.contentful.com/help/cms-as-code/).
To prepare your migration create a new file at `contentful-migrations/rich-text.js` and add the following code:
To prepare your migration create a new file at `contentful-migrations/rich-text.js` and add the following code:
```javascript
// contentful-migrations/rich-text.js
@@ -28,11 +30,11 @@ module.exports = function (migration, context) {
const richText = migration
.createContentType("richText")
.name("Rich Text")
.displayField("title");
.displayField("title")
richText.createField("title").name("Title (Internal)").type("Symbol");
richText.createField("body").name("Body").type("RichText");
};
richText.createField("title").name("Title (Internal)").type("Symbol")
richText.createField("body").name("Body").type("RichText")
}
```
This small snippet will create a content model in your Contentful space with two fields: a title which will be used to name entries in a meaningful manner (i.e. it won't be displayed to customers) and a body which contains the rich text to display. To apply your migration run:
@@ -41,7 +43,7 @@ This small snippet will create a content model in your Contentful space with two
yarn migrate:contentful --file contentful-migrations/rich-text.js
```
If you go to your Contentful space and click Content Model you will see that the Rich Text model has been added to your space:
If you go to your Contentful space and click Content Model you will see that the Rich Text model has been added to your space:
![](https://i.imgur.com/sCMjr4B.png)
The validation rules in the Page model only allow Hero and Tile Sections to be added to the Content Modules fields so you will need another migration to make it possible for pages to make use of the new Rich Text modules. Create a new migration at `contentful-migrations/update-page-module-validation.js` and add the following:
@@ -50,7 +52,7 @@ The validation rules in the Page model only allow Hero and Tile Sections to be a
// contentful-migrations/update-page-module-validation.js
module.exports = function (migration, context) {
const page = migration.editContentType("page");
const page = migration.editContentType("page")
page.editField("contentModules").items({
type: "Link",
@@ -60,24 +62,24 @@ module.exports = function (migration, context) {
linkContentType: ["hero", "tileSection", "richText"],
},
],
});
};
})
}
```
After migrating your space you are ready create your new contact page:
```shell
yarn migrate:contentful --file contentful-migrations/update-page-module-validation.js
```
```
## Adding Rich Text to About
To use your new Rich Text module **Content > Page > About**, and click **Add Content > Page**. You will now make use of the new Rich Text module to add some more details about your store. You can write your own text or use the text provided below if you just want to copy/paste.
> ### About Medusa
>
>
> Medusa is an open-source headless commerce engine for fast-growing businesses. Getting started with Medusa is very easy and you will be able to start selling online with a basic setup in no time, however, the real power of Medusa starts showing up when you add custom functionality and extend your core to fit your needs.
>
>
> The core Medusa package and all the official Medusa plugins ship as individual NPM packages that you install into a Node project. You store and plugins are configured in your medusa-config.js file making it very easy to manage your store as your business grows. Custom functionality doesn't have to come from plugins, you can also add project-level functionality by simply adding files in your `src/` folder. Medusa will automatically register your custom functionalities in the bootstrap phase.
![](https://i.imgur.com/hqiaoFq.png)
@@ -164,16 +166,18 @@ Restart your local Gatsby server and visit `http://localhost:8000/about`, you wi
![](https://i.imgur.com/8Teuxin.png)
## Enriching your Product pages
You have now seen how the Page model in Contentful can be extended to include a new content module in a reusable and modular manner. The same idea can be extended to your Product pages allowing you to create completely bespoke universes around your products. You will use the same techniques as above to create a Related Products section below the "Medusa Shirt" product.
### Migrating Products
First, add a new field to the Product content model. Using migrations you can create a file `contentful-migrations/product-add-modules.js`:
```javascript
// contentful-migrations/product-add-modules.js
module.exports = function (migration, context) {
const product = migration.editContentType("product");
const product = migration.editContentType("product")
product
.createField("contentModules")
@@ -187,19 +191,21 @@ module.exports = function (migration, context) {
linkContentType: ["hero", "tileSection", "richText"],
},
],
});
};
})
}
```
Run the migration:
```
yarn migrate:contentful --file contentful-migrations/product-add-modules.js
```
### Adding "Related Products" Tile Section
After the migration you can now add Content Modules to Products, to enrich the Product pages with relevant content. In this guide you will add a Tile Section that holds "Related Products", but the functionality could be further extended to showcase look book images, inspirational content or more detailed product descriptions.
In Contentful go to **Content > Product > Medusa Shirt** scroll all the way to the bottom, where you should be able to find the new *Content Modules* field:
In Contentful go to **Content > Product > Medusa Shirt** scroll all the way to the bottom, where you should be able to find the new _Content Modules_ field:
![](https://i.imgur.com/jUUpW9I.png)
@@ -207,19 +213,19 @@ Click **Add content > Tile Section** which will open a new Tile Section. For the
![](https://i.imgur.com/N7alMGz.png)
Click **Publish** and make sure that the Medusa Shirt product is published too.
Your data is now ready to be used in the storefront, but you still need to make a couple of changes to the storefront code to be able to view the new content.
## Adding Content Modules to Product pages
Just like you did for the Page component, you will have to fetch the Content Modules from Gatsby's GraphQL data layer.
Just like you did for the Page component, you will have to fetch the Content Modules from Gatsby's GraphQL data layer.
In the file `src/pages/products/{ContentfulProduct.handle}.js` add the following in the GraphQL query at the bottom of the file (e.g. after the variants query):
```graphql
# src/pages/products/{ContentfulProduct.handle}.js
contentModules {
... on ContentfulTileSection {
id
@@ -261,43 +267,43 @@ In the file `src/pages/products/{ContentfulProduct.handle}.js` add the following
}
```
This snippet will query the Content Modules defined for the product and will allow you to use the data in your components.
This snippet will query the Content Modules defined for the product and will allow you to use the data in your components.
Next open up the `src/views/products.jsx` file and add the following snippets.
Next open up the `src/views/products.jsx` file and add the following snippets.
Import the `TileSection` component:
```javascript
import TileSection from "../components/tile-section/tile-section"
```
Add the Content Modules in the JSX just before the final closing `div`:
Add the Content Modules in the JSX just before the final closing `div`:
```jsx
// src/views/products.jsx
<div className={styles.contentModules}>
{product.contentModules?.map((cm) => {
switch (cm.internal.type) {
case "ContentfulTileSection":
return <TileSection key={cm.id} data={cm} />
default:
return null
}
})}
</div>
// src/views/products.jsx
<div className={styles.contentModules}>
{product.contentModules?.map((cm) => {
switch (cm.internal.type) {
case "ContentfulTileSection":
return <TileSection key={cm.id} data={cm} />
default:
return null
}
})}
</div>
```
Restart the Gatsby server and visit http://localhost:8000/product/medusa-shirt you should now see the new "Related Products" Tile Section below the Product page controls.
![](https://i.imgur.com/AQHKA6j.png)
## Summary
In this guide you created a new content model for Rich Text input in Contentful using [contentful-migration](https://github.com/contentful/contentful-migration). You further extended the storefront to render the new Rich Text plugin. The concepts in this guide are meant to demonstrate how Contentful can be used to make your store more powerful in a modular and scalable way. The content modules covered in this guide could be further extended to add other custom modules, for example, you could add a Newsletter Signup, module that when encountered in the code renders a newsletter form.
## What's next
In the next part of this guide you will learn how to implement further commerce functionalities to your site such as adding support for discount codes, region based shopping and more. (Coming soon)
- [Deploying Medusa on Heroku](https://docs.medusa-commerce.com/how-to/deploying-on-heroku)

View File

@@ -79,7 +79,6 @@ describe("/admin/orders", () => {
await adminSeeder(dbConnection)
await orderSeeder(dbConnection)
await swapSeeder(dbConnection)
await claimSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
@@ -222,6 +221,48 @@ describe("/admin/orders", () => {
})
})
describe("POST /admin/orders/:id/swaps", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection)
await orderSeeder(dbConnection)
await claimSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("creates a swap on a claim", async () => {
const api = useApi()
const swapOnSwap = await api.post(
"/admin/orders/order-with-claim/swaps",
{
return_items: [
{
item_id: "test-item-co-2",
quantity: 1,
},
],
additional_items: [{ variant_id: "test-variant", quantity: 1 }],
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
expect(swapOnSwap.status).toEqual(200)
})
})
describe("POST /admin/orders/:id/claims", () => {
beforeEach(async () => {
try {
@@ -317,6 +358,43 @@ describe("/admin/orders", () => {
)
})
it("creates a claim on a claim", async () => {
const api = useApi()
const claimOnClaim = await api
.post(
"/admin/orders/order-with-claim/claims",
{
type: "replace",
claim_items: [
{
item_id: "test-item-co-2",
quantity: 1,
reason: "production_failure",
tags: ["fluff"],
images: ["https://test.image.com"],
},
],
additional_items: [
{
variant_id: "test-variant",
quantity: 1,
},
],
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(claimOnClaim.status).toEqual(200)
})
it("creates a claim with a shipping address", async () => {
const api = useApi()
@@ -840,6 +918,61 @@ describe("/admin/orders", () => {
})
})
describe("POST /admin/orders/:id/claims", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection)
await orderSeeder(dbConnection)
await swapSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("creates a claim on a swap", async () => {
const api = useApi()
const claimOnClaim = await api
.post(
"/admin/orders/order-with-swap/claims",
{
type: "replace",
claim_items: [
{
item_id: "return-item-1",
quantity: 1,
reason: "production_failure",
tags: ["fluff"],
images: ["https://test.image.com"],
},
],
additional_items: [
{
variant_id: "test-variant",
quantity: 1,
},
],
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(claimOnClaim.status).toEqual(200)
})
})
describe("POST /admin/orders/:id/return", () => {
let rrId
beforeEach(async () => {

View File

@@ -15,4 +15,4 @@ Object {
"phone": "12345678",
"updated_at": Any<String>,
}
`;
`;

View File

@@ -152,7 +152,7 @@ describe("/store/carts", () => {
expect.assertions(2)
const api = useApi()
let response = await api
await api
.post("/store/carts/test-cart", {
discounts: [{ code: "SPENT" }],
})

View File

@@ -150,6 +150,17 @@ module.exports = async (connection, data = {}) => {
await manager.save(d)
const usedDiscount = manager.create(Discount, {
id: "used-discount",
code: "USED",
is_dynamic: false,
is_disabled: false,
usage_limit: 1,
usage_count: 1,
})
await manager.save(usedDiscount)
const expiredRule = manager.create(DiscountRule, {
id: "expiredRule",
description: "expired rule",

View File

@@ -8,15 +8,15 @@
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "1.1.41-dev-1634111876218",
"medusa-interfaces": "1.1.21",
"@medusajs/medusa": "1.1.41-dev-1634202426468",
"medusa-interfaces": "1.1.23-dev-1634202426468",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"babel-preset-medusa-package": "1.1.13",
"babel-preset-medusa-package": "1.1.15-dev-1634202426468",
"jest": "^26.6.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _medusaInterfaces = require("medusa-interfaces");
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { return function () { var Super = _getPrototypeOf(Derived), result; if (_isNativeReflectConstruct()) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var ManualFulfillmentService = /*#__PURE__*/function (_FulfillmentService) {
_inherits(ManualFulfillmentService, _FulfillmentService);
var _super = _createSuper(ManualFulfillmentService);
function ManualFulfillmentService() {
_classCallCheck(this, ManualFulfillmentService);
return _super.call(this);
}
_createClass(ManualFulfillmentService, [{
key: "getFulfillmentOptions",
value: function getFulfillmentOptions() {
return [{
id: "manual-fulfillment"
}];
}
}, {
key: "validateFulfillmentData",
value: function validateFulfillmentData(data, cart) {
return data;
}
}, {
key: "validateOption",
value: function validateOption(data) {
return true;
}
}, {
key: "canCalculate",
value: function canCalculate() {
return false;
}
}, {
key: "calculatePrice",
value: function calculatePrice() {
throw Error("Manual Fulfillment service cannot calculatePrice");
}
}, {
key: "createOrder",
value: function createOrder() {
// No data is being sent anywhere
return;
}
}]);
return ManualFulfillmentService;
}(_medusaInterfaces.FulfillmentService);
_defineProperty(ManualFulfillmentService, "identifier", "manual");
var _default = ManualFulfillmentService;
exports["default"] = _default;

View File

@@ -27,6 +27,7 @@ const defaultRelations = [
"claims.additional_items",
"claims.fulfillments",
"claims.claim_items",
"claims.claim_items.item",
"claims.claim_items.images",
"swaps",
"swaps.return_order",
@@ -56,6 +57,7 @@ const defaultFields = [
"metadata",
"items.refundable",
"swaps.additional_items.refundable",
"claims.additional_items.refundable",
"shipping_total",
"discount_total",
"tax_total",

View File

@@ -241,6 +241,7 @@ export const defaultRelations = [
"claims.additional_items",
"claims.fulfillments",
"claims.claim_items",
"claims.claim_items.item",
"claims.claim_items.images",
// "claims.claim_items.tags",
"swaps",
@@ -271,6 +272,7 @@ export const defaultFields = [
"metadata",
"items.refundable",
"swaps.additional_items.refundable",
"claims.additional_items.refundable",
"shipping_total",
"discount_total",
"tax_total",

View File

@@ -108,7 +108,7 @@ export default async (req, res) => {
case "started": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async (manager) => {
async manager => {
const order = await orderService
.withTransaction(manager)
.retrieve(value.order_id, {
@@ -163,7 +163,7 @@ export default async (req, res) => {
case "swap_created": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async (manager) => {
async manager => {
const swaps = await swapService.list({
idempotency_key: idempotencyKey.idempotency_key,
})

View File

@@ -5,7 +5,7 @@ import { InventoryServiceMock } from "../__mocks__/inventory"
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -161,7 +161,7 @@ describe("SwapService", () => {
const cartService = {
create: jest.fn().mockReturnValue(Promise.resolve({ id: "cart" })),
update: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -171,10 +171,10 @@ describe("SwapService", () => {
})
const lineItemService = {
create: jest.fn().mockImplementation(d => Promise.resolve(d)),
update: jest.fn().mockImplementation(d => Promise.resolve(d)),
create: jest.fn().mockImplementation((d) => Promise.resolve(d)),
update: jest.fn().mockImplementation((d) => Promise.resolve(d)),
retrieve: () => Promise.resolve({}),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -199,6 +199,8 @@ describe("SwapService", () => {
"order.swaps.additional_items",
"order.discounts",
"order.discounts.rule",
"order.claims",
"order.claims.additional_items",
"additional_items",
"return_order",
"return_order.items",
@@ -304,7 +306,7 @@ describe("SwapService", () => {
const swapRepo = MockRepository()
const returnService = {
create: jest.fn().mockReturnValue(Promise.resolve({ id: "ret" })),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -401,7 +403,7 @@ describe("SwapService", () => {
receiveReturn: jest
.fn()
.mockReturnValue(Promise.resolve({ test: "received" })),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -447,7 +449,7 @@ describe("SwapService", () => {
receiveReturn: jest
.fn()
.mockReturnValue(Promise.resolve({ status: "requires_action" })),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -525,7 +527,7 @@ describe("SwapService", () => {
{ items: [{ item_id: "1234", quantity: 2 }], data: "new" },
])
),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -546,7 +548,7 @@ describe("SwapService", () => {
const lineItemService = {
update: jest.fn(),
retrieve: () => Promise.resolve({}),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -621,11 +623,11 @@ describe("SwapService", () => {
describe("cancelFulfillment", () => {
const swapRepo = MockRepository({
findOneWithRelations: () => Promise.resolve({}),
save: f => Promise.resolve(f),
save: (f) => Promise.resolve(f),
})
const fulfillmentService = {
cancelFulfillment: jest.fn().mockImplementation(f => {
cancelFulfillment: jest.fn().mockImplementation((f) => {
switch (f) {
case IdMap.getId("no-swap"):
return Promise.resolve({})
@@ -635,7 +637,7 @@ describe("SwapService", () => {
})
}
}),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -689,14 +691,14 @@ describe("SwapService", () => {
data: "new",
})
}),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -737,14 +739,14 @@ describe("SwapService", () => {
const lineItemService = {
update: jest.fn(),
retrieve: () => Promise.resolve({}),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const cartService = {
update: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -820,7 +822,7 @@ describe("SwapService", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -835,7 +837,7 @@ describe("SwapService", () => {
updateShippingMethod: () => {
return Promise.resolve()
},
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -844,7 +846,7 @@ describe("SwapService", () => {
update: () => {
return Promise.resolve()
},
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -859,14 +861,14 @@ describe("SwapService", () => {
cancelPayment: jest.fn(() => {
return Promise.resolve()
}),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const inventoryService = {
...InventoryServiceMock,
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -987,19 +989,19 @@ describe("SwapService", () => {
describe("success", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const paymentProviderService = {
capturePayment: jest.fn(g =>
capturePayment: jest.fn((g) =>
g.id === "good" ? Promise.resolve() : Promise.reject()
),
refundPayment: jest.fn(g =>
refundPayment: jest.fn((g) =>
g[0].id === "good" ? Promise.resolve() : Promise.reject()
),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -1127,7 +1129,7 @@ describe("SwapService", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -1190,7 +1192,7 @@ describe("SwapService", () => {
const paymentProviderService = {
cancelPayment: jest.fn(() => Promise.resolve({})),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -1222,7 +1224,7 @@ describe("SwapService", () => {
return Promise.resolve(swap)
}
},
save: f => f,
save: (f) => f,
})
const swapService = new SwapService({
@@ -1271,7 +1273,7 @@ describe("SwapService", () => {
it.each([["fail-refund-1"], ["fail-refund-2"], ["fail-refund-3"]])(
"fails to cancel swap when contains refund",
async input => {
async (input) => {
await expect(swapService.cancel(IdMap.getId(input))).rejects.toThrow(
"Swap with a refund cannot be canceled"
)

View File

@@ -286,6 +286,7 @@ class OrderService extends BaseService {
"refundable_amount",
"items.refundable",
"swaps.additional_items.refundable",
"claims.additional_items.refundable",
]
const totalsToSelect = select.filter((v) => totalFields.includes(v))
@@ -294,6 +295,8 @@ class OrderService extends BaseService {
relationSet.add("items")
relationSet.add("swaps")
relationSet.add("swaps.additional_items")
relationSet.add("claims")
relationSet.add("claims.additional_items")
relationSet.add("discounts")
relationSet.add("discounts.rule")
relationSet.add("discounts.rule.valid_for")
@@ -1407,6 +1410,22 @@ class OrderService extends BaseService {
}
}
if (
totalsFields.includes("claims.additional_items.refundable") &&
order.claims &&
order.claims.length
) {
for (const c of order.claims) {
c.additional_items = c.additional_items.map((i) => ({
...i,
refundable: this.totalsService_.getLineItemRefund(order, {
...i,
quantity: i.quantity - (i.returned_quantity || 0),
}),
}))
}
}
return order
}

View File

@@ -93,6 +93,12 @@ class ReturnService extends BaseService {
}
}
if (order.claims && order.claims.length) {
for (const c of order.claims) {
merged = [...merged, ...c.additional_items]
}
}
const toReturn = await Promise.all(
items.map(async data => {
const item = merged.find(i => i.id === data.item_id)
@@ -325,7 +331,13 @@ class ReturnService extends BaseService {
.withTransaction(manager)
.retrieve(orderId, {
select: ["refunded_total", "total", "refundable_amount"],
relations: ["swaps", "swaps.additional_items", "items"],
relations: [
"swaps",
"swaps.additional_items",
"claims",
"claims.additional_items",
"items",
],
})
const returnLines = await this.getFulfillmentItems_(
@@ -487,13 +499,18 @@ class ReturnService extends BaseService {
* @param {string[]} lineItems - the line items to return
* @return {Promise} the result of the update operation
*/
async receive(returnId, receivedItems, refundAmount, allowMismatch = false) {
async receive(
return_id,
received_items,
refund_amount,
allow_mismatch = false
) {
return this.atomicPhase_(async manager => {
const returnRepository = manager.getCustomRepository(
this.returnRepository_
)
const returnObj = await this.retrieve(returnId, {
const returnObj = await this.retrieve(return_id, {
relations: ["items", "swap", "swap.additional_items"],
})
@@ -537,7 +554,7 @@ class ReturnService extends BaseService {
const returnLines = await this.getFulfillmentItems_(
order,
receivedItems,
received_items,
this.validateReturnLineItem_
)
@@ -566,12 +583,12 @@ class ReturnService extends BaseService {
let returnStatus = "received"
const isMatching = newLines.every(l => l.is_requested)
if (!isMatching && !allowMismatch) {
if (!isMatching && !allow_mismatch) {
// Should update status
returnStatus = "requires_action"
}
const totalRefundableAmount = refundAmount || returnObj.refund_amount
const totalRefundableAmount = refund_amount || returnObj.refund_amount
const now = new Date()
const updateObj = {

View File

@@ -116,7 +116,7 @@ class SwapService extends BaseService {
"cart.total",
]
const totalsToSelect = select.filter(v => totalFields.includes(v))
const totalsToSelect = select.filter((v) => totalFields.includes(v))
if (totalsToSelect.length > 0) {
const relationSet = new Set(relations)
relationSet.add("cart")
@@ -127,7 +127,7 @@ class SwapService extends BaseService {
relationSet.add("cart.region")
relations = [...relationSet]
select = select.filter(v => !totalFields.includes(v))
select = select.filter((v) => !totalFields.includes(v))
}
return {
@@ -170,9 +170,8 @@ class SwapService extends BaseService {
const validatedId = this.validateId_(id)
const { totalsToSelect, ...newConfig } = this.transformQueryForTotals_(
config
)
const { totalsToSelect, ...newConfig } =
this.transformQueryForTotals_(config)
const query = this.buildQuery_({ id: validatedId }, newConfig)
@@ -251,7 +250,7 @@ class SwapService extends BaseService {
*/
validateReturnItems_(order, returnItems) {
return returnItems.map(({ item_id, quantity }) => {
const item = order.items.find(i => i.id === item_id)
const item = order.items.find((i) => i.id === item_id)
// The item must exist in the order
if (!item) {
@@ -303,7 +302,7 @@ class SwapService extends BaseService {
}
) {
const { no_notification, ...rest } = custom
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
if (
order.fulfillment_status === "not_fulfilled" ||
order.payment_status !== "captured"
@@ -318,7 +317,6 @@ class SwapService extends BaseService {
const line = await this.lineItemService_.retrieve(item.item_id, {
relations: ["order", "swap", "claim_order"],
})
console.log(line)
if (
line.order?.canceled_at ||
@@ -377,7 +375,7 @@ class SwapService extends BaseService {
}
async processDifference(swapId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: ["payment", "order", "order.payments"],
})
@@ -493,7 +491,7 @@ class SwapService extends BaseService {
}
async update(swapId, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId)
if ("metadata" in update) {
@@ -525,7 +523,7 @@ class SwapService extends BaseService {
* the new cart.
*/
async createCart(swapId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: [
"order",
@@ -534,6 +532,8 @@ class SwapService extends BaseService {
"order.swaps.additional_items",
"order.discounts",
"order.discounts.rule",
"order.claims",
"order.claims.additional_items",
"additional_items",
"return_order",
"return_order.items",
@@ -607,7 +607,13 @@ class SwapService extends BaseService {
}
}
const lineItem = allItems.find(i => i.id === r.item_id)
if (order.claims && order.claims.length) {
for (const c of order.claims) {
allItems = [...allItems, ...c.additional_items]
}
}
const lineItem = allItems.find((i) => i.id === r.item_id)
const toCreate = {
cart_id: cart.id,
@@ -638,7 +644,7 @@ class SwapService extends BaseService {
*
*/
async registerCartCompletion(swapId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: [
"cart",
@@ -770,7 +776,7 @@ class SwapService extends BaseService {
* status.
*/
async receiveReturn(swapId, returnItems) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, { relations: ["return_order"] })
if (swap.canceled_at) {
@@ -811,7 +817,7 @@ class SwapService extends BaseService {
* @returns {Promise<Swap>} the canceled swap.
*/
async cancel(swapId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: ["payment", "fulfillments", "return_order"],
})
@@ -877,7 +883,7 @@ class SwapService extends BaseService {
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: [
"payment",
@@ -937,7 +943,7 @@ class SwapService extends BaseService {
is_swap: true,
no_notification: evaluatedNoNotification,
},
swap.additional_items.map(i => ({
swap.additional_items.map((i) => ({
item_id: i.id,
quantity: i.quantity,
})),
@@ -954,7 +960,7 @@ class SwapService extends BaseService {
// Update all line items to reflect fulfillment
for (const item of swap.additional_items) {
const fulfillmentItem = successfullyFulfilled.find(
f => item.id === f.item_id
(f) => item.id === f.item_id
)
if (fulfillmentItem) {
@@ -999,7 +1005,7 @@ class SwapService extends BaseService {
* @returns updated swap
*/
async cancelFulfillment(fulfillmentId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const canceled = await this.fulfillmentService_
.withTransaction(manager)
.cancelFulfillment(fulfillmentId)
@@ -1042,7 +1048,7 @@ class SwapService extends BaseService {
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: ["additional_items"],
})
@@ -1068,7 +1074,7 @@ class SwapService extends BaseService {
// Go through all the additional items in the swap
for (const i of swap.additional_items) {
const shipped = shipment.items.find(si => si.item_id === i.id)
const shipped = shipment.items.find((si) => si.item_id === i.id)
if (shipped) {
const shippedQty = (i.shipped_quantity || 0) + shipped.quantity
await this.lineItemService_.withTransaction(manager).update(i.id, {
@@ -1117,7 +1123,7 @@ class SwapService extends BaseService {
const keyPath = `metadata.${key}`
return this.swapModel_
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
.catch(err => {
.catch((err) => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
@@ -1130,7 +1136,7 @@ class SwapService extends BaseService {
* @returns {Promise<Order>} the resulting order
*/
async registerReceived(id) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(id, {
relations: ["return_order", "return_order.items"],
})

View File

@@ -1,6 +1,7 @@
import _ from "lodash"
import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils"
import carts from "../api/routes/store/carts"
/**
* A service that calculates total and subtotals for orders, carts etc..
@@ -158,6 +159,13 @@ class TotalsService extends BaseService {
}
}
if (order.claims && order.claims.length) {
for (const c of order.claims) {
const claimItemIds = c.additional_items.map(el => el.id)
itemIds = [...itemIds, ...claimItemIds]
}
}
const refunds = lineItems.map(i => {
if (!itemIds.includes(i.id)) {
throw new MedusaError(
@@ -253,6 +261,12 @@ class TotalsService extends BaseService {
}
}
if (cart.claims && cart.claims.length) {
for (const c of cart.claims) {
merged = [...merged, ...c.additional_items]
}
}
const { type, allocation, value } = discount.rule
if (allocation === "total") {
let percentage = 0