feat(medusa,brightpearl,segment,webshipper): claims (#163)

* chore: create tests

* chore: models

* fix: passing initial tests

* test: adds integration test

* test: clean up integration implementation

* fix: claims

* fix: brightpearl + webshipper

* tests: passing

* fix: update claim items

* fix: adds gitignore

* fix: pr comments

* fix: single migration

* fix(medusa-plugin-segment): adds item claimed event to segment
This commit is contained in:
Sebastian Rindom
2021-02-03 09:49:12 +01:00
committed by GitHub
parent 0e0dfea571
commit 690d339667
60 changed files with 9429 additions and 189 deletions
+13
View File
@@ -0,0 +1,13 @@
let ignore = [`**/dist`];
// Jest needs to compile this code, but generally we don't want this copied
// to output folders
if (process.env.NODE_ENV !== `test`) {
ignore.push(`**/__tests__`);
}
module.exports = {
sourceMaps: true,
presets: ["babel-preset-medusa-package"],
ignore,
};
+4
View File
@@ -0,0 +1,4 @@
dist
node_modules
*yarn-error.log
@@ -0,0 +1,469 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db");
const orderSeeder = require("../../helpers/order-seeder");
const adminSeeder = require("../../helpers/admin-seeder");
jest.setTimeout(30000);
describe("/admin/orders", () => {
let medusaProcess;
let dbConnection;
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."));
dbConnection = await initDb({ cwd });
medusaProcess = await setupServer({ cwd });
});
afterAll(async () => {
await dbConnection.close();
await dropDatabase({ databaseName: "medusa-integration" });
medusaProcess.kill();
});
describe("GET /admin/orders", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection);
await orderSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "fulfillment"`);
await manager.query(`DELETE FROM "swap"`);
await manager.query(`DELETE FROM "return"`);
await manager.query(`DELETE FROM "claim_image"`);
await manager.query(`DELETE FROM "claim_tag"`);
await manager.query(`DELETE FROM "claim_item"`);
await manager.query(`DELETE FROM "claim_order"`);
await manager.query(`DELETE FROM "line_item"`);
await manager.query(`DELETE FROM "money_amount"`);
await manager.query(`DELETE FROM "product_variant"`);
await manager.query(`DELETE FROM "product"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "shipping_option"`);
await manager.query(`DELETE FROM "discount"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "order"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(`DELETE FROM "region"`);
await manager.query(`DELETE FROM "user"`);
});
it("creates a cart", async () => {
const api = useApi();
const response = await api
.get("/admin/orders", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
});
});
describe("POST /admin/orders/:id/claims", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection);
await orderSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "fulfillment_item"`);
await manager.query(`DELETE FROM "fulfillment"`);
await manager.query(`DELETE FROM "swap"`);
await manager.query(`DELETE FROM "return"`);
await manager.query(`DELETE FROM "claim_image"`);
await manager.query(`DELETE FROM "claim_tag"`);
await manager.query(`DELETE FROM "claim_item"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "line_item"`);
await manager.query(`DELETE FROM "claim_order"`);
await manager.query(`DELETE FROM "money_amount"`);
await manager.query(`DELETE FROM "product_variant"`);
await manager.query(`DELETE FROM "product"`);
await manager.query(`DELETE FROM "shipping_option"`);
await manager.query(`DELETE FROM "discount"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "order"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(`DELETE FROM "region"`);
await manager.query(`DELETE FROM "user"`);
});
it("creates a claim", async () => {
const api = useApi();
const response = await api.post(
"/admin/orders/test-order/claims",
{
type: "replace",
claim_items: [
{
item_id: "test-item",
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",
},
}
);
expect(response.status).toEqual(200);
expect(response.data.order.claims[0].claim_items).toEqual([
expect.objectContaining({
item_id: "test-item",
quantity: 1,
reason: "production_failure",
images: [
expect.objectContaining({
url: "https://test.image.com",
}),
],
tags: [
expect.objectContaining({
value: "fluff",
}),
],
}),
]);
expect(response.data.order.claims[0].additional_items).toEqual([
expect.objectContaining({
variant_id: "test-variant",
quantity: 1,
}),
]);
});
it("updates a claim", async () => {
const api = useApi();
const response = await api.post(
"/admin/orders/test-order/claims",
{
type: "replace",
claim_items: [
{
item_id: "test-item",
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",
},
}
);
expect(response.status).toEqual(200);
const cid = response.data.order.claims[0].id;
const { status, data: updateData } = await api.post(
`/admin/orders/test-order/claims/${cid}`,
{
shipping_methods: [
{
id: "test-method",
},
],
},
{
headers: {
authorization: "bearer test_token",
},
}
);
expect(status).toEqual(200);
expect(updateData.order.claims[0].shipping_methods).toEqual([
expect.objectContaining({
id: "test-method",
}),
]);
});
it("updates claim items", async () => {
const api = useApi();
const response = await api.post(
"/admin/orders/test-order/claims",
{
type: "replace",
claim_items: [
{
item_id: "test-item",
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",
},
}
);
expect(response.status).toEqual(200);
let claim = response.data.order.claims[0];
const cid = claim.id;
const { status, data: updateData } = await api.post(
`/admin/orders/test-order/claims/${cid}`,
{
claim_items: claim.claim_items.map((i) => ({
id: i.id,
note: "Something new",
images: [
...i.images.map((i) => ({ id: i.id })),
{ url: "https://new.com/image" },
],
tags: [
{ value: "completely" },
{ value: "NEW" },
{ value: " tags" },
],
})),
},
{
headers: {
authorization: "bearer test_token",
},
}
);
expect(status).toEqual(200);
expect(updateData.order.claims.length).toEqual(1);
claim = updateData.order.claims[0];
expect(claim.claim_items.length).toEqual(1);
expect(claim.claim_items).toEqual([
expect.objectContaining({
id: claim.claim_items[0].id,
reason: "production_failure",
note: "Something new",
images: expect.arrayContaining([
expect.objectContaining({
url: "https://test.image.com",
}),
expect.objectContaining({
url: "https://new.com/image",
}),
]),
tags: expect.arrayContaining([
expect.objectContaining({ value: "completely" }),
expect.objectContaining({ value: "new" }),
expect.objectContaining({ value: "tags" }),
]),
}),
]);
});
it("updates claim items - removes image", async () => {
const api = useApi();
const response = await api.post(
"/admin/orders/test-order/claims",
{
type: "replace",
claim_items: [
{
item_id: "test-item",
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",
},
}
);
expect(response.status).toEqual(200);
let claim = response.data.order.claims[0];
const cid = claim.id;
const { status, data: updateData } = await api.post(
`/admin/orders/test-order/claims/${cid}`,
{
claim_items: claim.claim_items.map((i) => ({
id: i.id,
note: "Something new",
images: [],
tags: [
{ value: "completely" },
{ value: "NEW" },
{ value: " tags" },
],
})),
},
{
headers: {
authorization: "bearer test_token",
},
}
);
expect(status).toEqual(200);
expect(updateData.order.claims.length).toEqual(1);
claim = updateData.order.claims[0];
expect(claim.claim_items.length).toEqual(1);
expect(claim.claim_items).toEqual([
expect.objectContaining({
id: claim.claim_items[0].id,
reason: "production_failure",
note: "Something new",
images: [],
tags: expect.arrayContaining([
expect.objectContaining({ value: "completely" }),
expect.objectContaining({ value: "new" }),
expect.objectContaining({ value: "tags" }),
]),
}),
]);
});
it("fulfills a claim", async () => {
const api = useApi();
const response = await api
.post(
"/admin/orders/test-order/claims",
{
type: "replace",
shipping_methods: [
{
id: "test-method",
},
],
claim_items: [
{
item_id: "test-item",
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);
});
const cid = response.data.order.claims[0].id;
const fulRes = await api.post(
`/admin/orders/test-order/claims/${cid}/fulfillments`,
{},
{
headers: {
Authorization: "Bearer test_token",
},
}
);
expect(fulRes.status).toEqual(200);
expect(fulRes.data.order.claims).toEqual([
expect.objectContaining({
id: cid,
order_id: "test-order",
fulfillment_status: "fulfilled",
}),
]);
const fid = fulRes.data.order.claims[0].fulfillments[0].id;
const iid = fulRes.data.order.claims[0].additional_items[0].id;
expect(fulRes.data.order.claims[0].fulfillments).toEqual([
expect.objectContaining({
items: [
{
fulfillment_id: fid,
item_id: iid,
quantity: 1,
},
],
}),
]);
});
});
});
@@ -0,0 +1,74 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const { Region } = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db");
jest.setTimeout(30000);
describe("/store/carts", () => {
let medusaProcess;
let dbConnection;
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."));
dbConnection = await initDb({ cwd });
medusaProcess = await setupServer({ cwd });
});
afterAll(async () => {
dbConnection.close();
dropDatabase({ databaseName: "medusa-integration" });
medusaProcess.kill();
});
describe("POST /store/carts", () => {
beforeEach(async () => {
const manager = dbConnection.manager;
await manager.insert(Region, {
id: "region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
});
await manager.query(
`UPDATE "country" SET region_id='region' WHERE iso_2 = 'us'`
);
});
afterEach(async () => {
const manager = dbConnection.manager;
await manager.query(`DELETE FROM "cart"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(`DELETE FROM "region"`);
});
it("creates a cart", async () => {
const api = useApi();
const response = await api.post("/store/carts");
expect(response.status).toEqual(200);
const getRes = await api.post(`/store/carts/${response.data.cart.id}`);
expect(getRes.status).toEqual(200);
});
it("creates a cart with country", async () => {
const api = useApi();
const response = await api.post("/store/carts", {
country_code: "us",
});
expect(response.status).toEqual(200);
expect(response.data.cart.shipping_address.country_code).toEqual("us");
const getRes = await api.post(`/store/carts/${response.data.cart.id}`);
expect(getRes.status).toEqual(200);
});
});
});
@@ -0,0 +1,17 @@
const Scrypt = require("scrypt-kdf");
const { User } = require("@medusajs/medusa");
module.exports = async (connection, data = {}) => {
const manager = connection.manager;
const buf = await Scrypt.kdf("secret_password", { logN: 1, r: 1, p: 1 });
const password_hash = buf.toString("base64");
await manager.insert(User, {
id: "admin_user",
email: "admin@medusa.js",
api_token: "test_token",
password_hash,
...data,
});
};
@@ -0,0 +1,147 @@
const {
ShippingProfile,
Customer,
MoneyAmount,
LineItem,
Country,
ShippingOption,
ShippingMethod,
Product,
ProductVariant,
Region,
Order,
} = require("@medusajs/medusa");
module.exports = async (connection, data = {}) => {
const manager = connection.manager;
const defaultProfile = await manager.findOne(ShippingProfile, {
type: "default",
});
await manager.insert(Product, {
id: "test-product",
title: "test product",
profile_id: defaultProfile.id,
options: [{ id: "test-option", title: "Size" }],
});
await manager.insert(ProductVariant, {
id: "test-variant",
title: "test variant",
product_id: "test-product",
inventory_quantity: 1,
options: [
{
option_id: "test-option",
value: "Size",
},
],
});
const ma = manager.create(MoneyAmount, {
variant_id: "test-variant",
currency_code: "usd",
amount: 8000,
});
await manager.save(ma);
await manager.insert(Region, {
id: "test-region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
});
await manager.query(
`UPDATE "country" SET region_id='test-region' WHERE iso_2 = 'us'`
);
await manager.insert(Customer, {
id: "test-customer",
email: "test@email.com",
});
await manager.insert(ShippingOption, {
id: "test-option",
name: "test-option",
provider_id: "test-ful",
region_id: "test-region",
profile_id: defaultProfile.id,
price_type: "flat_rate",
amount: 1000,
data: {},
});
const order = manager.create(Order, {
id: "test-order",
customer_id: "test-customer",
email: "test@email.com",
billing_address: {
id: "test-billing-address",
first_name: "lebron",
},
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
tax_rate: 0,
discounts: [
{
id: "test-discount",
code: "TEST134",
is_dynamic: false,
rule: {
id: "test-rule",
description: "Test Discount",
type: "percentage",
value: 10,
allocation: "total",
},
is_disabled: false,
regions: [
{
id: "test-region",
},
],
},
],
payments: [
{
id: "test-payment",
amount: 10000,
currency_code: "usd",
amount_refunded: 0,
provider_id: "test",
data: {},
},
],
items: [
{
id: "test-item",
fulfilled_quantity: 1,
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant",
},
],
...data,
});
await manager.save(order);
await manager.insert(ShippingMethod, {
id: "test-method",
order_id: "test-order",
shipping_option_id: "test-option",
order_id: "test-order",
price: 1000,
data: {},
});
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
setupFilesAfterEnv: ["<rootDir>/setup.js"],
};
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
plugins: [],
projectConfig: {
// redis_url: REDIS_URL,
database_url: "postgres://localhost/medusa-integration",
database_type: "postgres",
},
};
+19
View File
@@ -0,0 +1,19 @@
{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "^1.1.3",
"medusa-interfaces": "^1.1.0"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"babel-preset-medusa-package": "^1.1.0"
}
}
@@ -0,0 +1,49 @@
import { FulfillmentService } from "medusa-interfaces";
class TestFulService extends FulfillmentService {
static identifier = "test-ful";
constructor() {
super();
}
getFulfillmentOptions() {
return [
{
id: "manual-fulfillment",
},
];
}
validateFulfillmentData(data, cart) {
return data;
}
validateOption(data) {
return true;
}
canCalculate() {
return false;
}
calculatePrice() {
throw Error("Manual Fulfillment service cannot calculatePrice");
}
createOrder() {
// No data is being sent anywhere
return Promise.resolve({});
}
createFulfillment() {
// No data is being sent anywhere
return Promise.resolve({});
}
cancelFulfillment() {
return Promise.resolve({});
}
}
export default TestFulService;
@@ -0,0 +1,59 @@
import { PaymentService } from "medusa-interfaces";
class TestPayService extends PaymentService {
static identifier = "test-pay";
constructor() {
super();
}
async getStatus(paymentData) {
return "authorized";
}
async retrieveSavedMethods(customer) {
return Promise.resolve([]);
}
async createPayment() {
return {};
}
async retrievePayment(data) {
return {};
}
async getPaymentData(sessionData) {
return {};
}
async authorizePayment(sessionData, context = {}) {
return {};
}
async updatePaymentData(sessionData, update) {
return {};
}
async updatePayment(sessionData, cart) {
return {};
}
async deletePayment(payment) {
return {};
}
async capturePayment(payment) {
return {};
}
async refundPayment(payment, amountToRefund) {
return {};
}
async cancelPayment(payment) {
return {};
}
}
export default TestPayService;
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
const path = require("path");
const { spawn } = require("child_process");
const { setPort } = require("./use-api");
module.exports = ({ cwd }) => {
const serverPath = path.join(__dirname, "test-server.js");
return new Promise((resolve, reject) => {
const medusaProcess = spawn("node", [path.resolve(serverPath)], {
cwd,
env: {
...process.env,
NODE_ENV: "development",
JWT_SECRET: "test",
COOKIE_SECRET: "test",
},
stdio: ["ignore", "ignore", "inherit", "ipc"],
});
medusaProcess.on("uncaughtException", (err) => {
medusaProcess.kill();
});
medusaProcess.on("message", (port) => {
setPort(port);
resolve(medusaProcess);
});
});
};
+32
View File
@@ -0,0 +1,32 @@
const path = require("path");
const express = require("express");
const getPort = require("get-port");
const loaders = require("@medusajs/medusa/dist/loaders").default;
const initialize = async () => {
const app = express();
const { dbConnection } = await loaders({
directory: path.resolve(process.cwd()),
expressApp: app,
});
const PORT = await getPort();
return {
db: dbConnection,
app,
port: PORT,
};
};
const setup = async () => {
const { app, port } = await initialize();
app.listen(port, (err) => {
process.send(port);
});
};
setup();
+21
View File
@@ -0,0 +1,21 @@
const axios = require("axios").default;
const ServerTestUtil = {
port_: null,
client_: null,
setPort: function (port) {
this.client_ = axios.create({ baseURL: `http://localhost:${port}` });
},
};
const instance = ServerTestUtil;
module.exports = {
setPort: function (port) {
instance.setPort(port);
},
useApi: function () {
return instance.client_;
},
};
+73
View File
@@ -0,0 +1,73 @@
const { dropDatabase, createDatabase } = require("pg-god");
const { createConnection } = require("typeorm");
const path = require("path");
const DbTestUtil = {
db_: null,
setDb: function (connection) {
this.db_ = connection;
},
clear: function () {
return this.db_.synchronize(true);
},
shutdown: async function () {
await this.db_.close();
return dropDatabase({ databaseName });
},
};
const instance = DbTestUtil;
module.exports = {
initDb: async function ({ cwd }) {
const migrationDir = path.resolve(
path.join(
cwd,
`node_modules`,
`@medusajs`,
`medusa`,
`dist`,
`migrations`
)
);
const databaseName = "medusa-integration";
await createDatabase({ databaseName });
const connection = await createConnection({
type: "postgres",
url: "postgres://localhost/medusa-integration",
migrations: [`${migrationDir}/*.js`],
});
await connection.runMigrations();
await connection.close();
const modelsLoader = require(path.join(
cwd,
`node_modules`,
`@medusajs`,
`medusa`,
`dist`,
`loaders`,
`models`
)).default;
const entities = modelsLoader({}, { register: false });
const dbConnection = await createConnection({
type: "postgres",
url: "postgres://localhost/medusa-integration",
entities,
});
instance.setDb(dbConnection);
return dbConnection;
},
useDb: function () {
return instance;
},
};
+41
View File
@@ -0,0 +1,41 @@
const ServerTestUtil = {
server_: null,
app_: null,
setApp: function (app) {
this.app_ = app;
},
start: async function () {
this.server_ = await new Promise((resolve, reject) => {
const s = this.app_.listen(PORT, (err) => {
if (err) {
reject(err);
}
});
resolve(s);
});
},
kill: function () {
return new Promise((resolve, _) => {
if (this.server_) {
this.server_.close(() => resolve());
}
resolve();
});
},
};
const instance = ServerTestUtil;
module.exports = {
setApp: function (app) {
instance.setApp(app);
return instance;
},
useServer: function () {
return instance;
},
};
+21
View File
@@ -0,0 +1,21 @@
const glob = require(`glob`);
const pkgs = glob
.sync(`${__dirname}/*/`)
.map((p) => p.replace(__dirname, `<rootDir>/integration-tests`));
module.exports = {
testEnvironment: `node`,
rootDir: `../`,
roots: pkgs,
testPathIgnorePatterns: [
`/examples/`,
`/www/`,
`/dist/`,
`/node_modules/`,
`__tests__/fixtures`,
`__testfixtures__`,
`.cache`,
],
transform: { "^.+\\.[jt]s$": `<rootDir>/jest-transformer.js` },
};
+9 -2
View File
@@ -11,18 +11,25 @@
"@babel/preset-env": "^7.11.5",
"@babel/register": "^7.11.5",
"@babel/runtime": "^7.11.2",
"axios": "^0.21.1",
"axios-mock-adapter": "^1.19.0",
"babel-jest": "^26.6.3",
"babel-preset-medusa-package": "^1.0.0",
"cross-env": "^7.0.2",
"express": "^4.17.1",
"get-port": "^5.1.1",
"jest": "^26.6.3",
"lerna": "^3.22.1",
"mongoose": "^5.10.15",
"prettier": "^2.1.1"
"pg-god": "^1.0.11",
"prettier": "^2.1.1",
"resolve-cwd": "^3.0.0",
"typeorm": "^0.2.30"
},
"scripts": {
"bootstrap": "lerna bootstrap",
"jest": "jest",
"test": "jest"
"test": "jest",
"test:integration": "jest --config=integration-tests/jest.config.js --runInBand"
}
}
@@ -4,7 +4,7 @@ import Webshipper from "../utils/webshipper"
class WebshipperFulfillmentService extends FulfillmentService {
static identifier = "webshipper"
constructor({ logger, swapService, orderService }, options) {
constructor({ logger, claimService, swapService, orderService }, options) {
super()
this.options_ = options
@@ -18,6 +18,9 @@ class WebshipperFulfillmentService extends FulfillmentService {
/** @private @const {SwapService} */
this.swapService_ = swapService
/** @private @const {SwapService} */
this.claimService_ = claimService
/** @private @const {AxiosClient} */
this.client_ = new Webshipper({
account: this.options_.account,
@@ -82,19 +85,20 @@ class WebshipperFulfillmentService extends FulfillmentService {
* return lines.
*/
async createReturn(returnOrder) {
let fromOrder
let orderId
if (returnOrder.order_id) {
fromOrder = await this.orderService_.retrieve(returnOrder.order_id, {
select: ["total"],
relations: ["shipping_address", "returns"],
})
orderId = returnOrder.order_id
} else if (returnOrder.swap) {
fromOrder = await this.orderService_.retrieve(returnOrder.swap.order_id, {
select: ["total"],
relations: ["shipping_address", "returns"],
})
orderId = returnOrder.swap.order_id
} else if (returnOrder.claim_order) {
orderId = returnOrder.claim_order.order_id
}
const fromOrder = await this.orderService_.retrieve(orderId, {
select: ["total"],
relations: ["discounts", "shipping_address", "returns"],
})
const methodData = returnOrder.shipping_method.data
const relationships = {
@@ -384,6 +388,12 @@ class WebshipperFulfillmentService extends FulfillmentService {
trackingNumbers
)
}
} else if (orderId.charAt(0).toLowerCase() === "c") {
return this.claimService_.createShipment(
orderId,
fulfillmentIndex,
trackingNumbers
)
} else {
if (fulfillmentIndex.startsWith("ful")) {
return this.orderService_.createShipment(
@@ -11,6 +11,7 @@ class BrightpearlService extends BaseService {
regionService,
orderService,
swapService,
claimService,
discountService,
},
options
@@ -25,6 +26,7 @@ class BrightpearlService extends BaseService {
this.discountService_ = discountService
this.oauthService_ = oauthService
this.swapService_ = swapService
this.claimService_ = claimService
}
async getClient() {
@@ -609,6 +611,63 @@ class BrightpearlService extends BaseService {
})
}
async createClaimCredit(fromOrder, fromClaim) {
const region = fromOrder.region
const client = await this.getClient()
const authData = await this.getAuthData()
const orderId = fromOrder.metadata.brightpearl_sales_order_id
const cIndex = fromOrder.claims.findIndex((c) => fromClaim.id === c.id)
if (orderId) {
const parentSo = await client.orders.retrieve(orderId)
const order = {
currency: parentSo.currency,
ref: `${parentSo.ref}-C${cIndex + 1}`,
externalRef: `${parentSo.externalRef}.${fromClaim.id}`,
channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id,
customer: parentSo.customer,
delivery: parentSo.delivery,
parentId: orderId,
rows: [
{
name: `#${fromOrder.display_id}: Claim ${fromClaim.id}`,
net: this.bpnum_(
fromClaim.refund_amount,
10000 / (100 + fromOrder.tax_rate)
),
tax: this.bpnum_(
fromClaim.refund_amount * (1 - 100 / (100 + fromOrder.tax_rate))
),
taxCode: region.tax_code,
nominalCode: this.options.sales_account_code || `4000`,
quantity: 1,
},
],
}
return client.orders
.createCredit(order)
.then(async (creditId) => {
const paymentMethod = fromOrder.payments[0]
const paymentType = "PAYMENT"
const payment = {
transactionRef: `${paymentMethod.id}.${paymentType}-${fromClaim.id}`,
transactionCode: fromClaim.id,
paymentMethodCode: this.options.payment_method_code || "1220",
orderId: creditId,
currencyIsoCode: fromOrder.currency_code.toUpperCase(),
amountPaid: this.bpnum_(fromClaim.refund_amount),
paymentDate: new Date(),
paymentType,
}
await client.payments.create(payment)
})
.catch((err) => console.log(err.response.data.errors))
}
}
async createSwapCredit(fromOrder, fromSwap) {
const region = fromOrder.region
const client = await this.getClient()
@@ -717,7 +776,10 @@ class BrightpearlService extends BaseService {
return client.payments.create(payment)
}
async getBrightpearlRows(fromOrder) {
async getBrightpearlRows(
fromOrder,
config = { include_price: true, is_claim: false }
) {
const { region } = fromOrder
const discount = fromOrder.discounts.find(
({ rule }) => rule.type !== "free_shipping"
@@ -741,15 +803,27 @@ class BrightpearlService extends BaseService {
} else {
row.name = item.title
}
row.net = this.bpnum_(item.unit_price * item.quantity - ld.amount)
row.tax = this.bpnum_(
item.unit_price * item.quantity - ld.amount,
fromOrder.tax_rate
)
if (config.include_price) {
row.net = this.bpnum_(item.unit_price * item.quantity - ld.amount)
row.tax = this.bpnum_(
item.unit_price * item.quantity - ld.amount,
fromOrder.tax_rate
)
} else if (config.is_claim) {
row.net = await this.retrieveProductPrice(
bpProduct.productId,
this.options.cost_price_list || `1`
)
row.tax = this.bpnum_(row.net * 100, fromOrder.tax_rate)
}
row.quantity = item.quantity
row.taxCode = region.tax_code
row.externalRef = item.id
row.nominalCode = this.options.sales_account_code || "4000"
row.nominalCode = config.is_claim
? this.options.claims_account_code || "4000"
: this.options.sales_account_code || "4000"
if (item.is_giftcard) {
row.nominalCode = this.options.gift_card_account_code || "4000"
@@ -793,6 +867,106 @@ class BrightpearlService extends BaseService {
return lines
}
async createClaim(fromOrder, fromClaim) {
const client = await this.getClient()
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
if (!customer) {
customer = await this.createCustomer({
...fromOrder,
...fromClaim,
})
}
const authData = await this.getAuthData()
const cIndex = fromOrder.claims.findIndex((s) => fromClaim.id === s.id)
const { shipping_address } = fromClaim
const order = {
currency: {
code: this.options.base_currency || `EUR`,
},
priceListId: this.options.cost_price_list || `1`,
ref: `${fromOrder.display_id}-C${cIndex + 1}`,
externalRef: `${fromOrder.id}.${fromClaim.id}`,
channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id,
statusId:
this.options.claim_status_id || this.options.default_status_id || `3`,
customer: {
id: customer.contactId,
address: {
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
addressLine1: shipping_address.address_1,
addressLine2: shipping_address.address_2,
postalCode: shipping_address.postal_code,
countryIsoCode: shipping_address.country_code,
telephone: shipping_address.phone,
email: fromOrder.email,
},
},
delivery: {
shippingMethodId: 0,
address: {
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
addressLine1: shipping_address.address_1,
addressLine2: shipping_address.address_2,
postalCode: shipping_address.postal_code,
countryIsoCode: shipping_address.country_code,
telephone: shipping_address.phone,
email: fromOrder.email,
},
},
rows: await this.getBrightpearlRows(
{
region: fromOrder.region,
discounts: fromOrder.discounts,
tax_rate: fromOrder.tax_rate,
items: fromClaim.additional_items,
shipping_methods: [],
},
{ include_price: false, is_claim: true }
),
}
return client.orders
.create(order)
.then(async (salesOrderId) => {
const order = await client.orders.retrieve(salesOrderId)
await client.warehouses.createReservation(order, this.options.warehouse)
const total = order.rows.reduce((acc, next) => {
return acc + parseFloat(next.net) + parseFloat(next.tax)
}, 0)
const paymentMethod = fromOrder.payments[0]
const paymentType = "RECEIPT"
const payment = {
transactionRef: `${paymentMethod.id}.${paymentType}-${fromClaim.id}`,
transactionCode: fromOrder.id,
paymentMethodCode: this.options.payment_method_code || "1220",
orderId: salesOrderId,
currencyIsoCode: this.options.base_currency || "EUR",
amountPaid: total,
paymentDate: new Date(),
paymentType,
}
await client.payments.create(payment)
return this.claimService_.update(fromClaim.id, {
metadata: {
brightpearl_sales_order_id: salesOrderId,
},
})
})
.catch((err) => {
// console.log(err.response.data)
throw err
})
}
async retrieveCustomerByEmail(email) {
const client = await this.getClient()
return client.customers.retrieveByEmail(email).then((customers) => {
@@ -818,6 +992,20 @@ class BrightpearlService extends BaseService {
})
}
async retrieveProductPrice(id, priceList) {
const client = await this.getClient()
return client.products.retrievePrice(id).then((data) => {
if (!data.priceLists.length) {
return null
} else {
const found = data.priceLists.find(
(p) => `${p.priceListId}` === `${priceList}`
)
return found.quantityPrice["1"]
}
})
}
async createFulfillmentFromGoodsOut(id) {
const client = await this.getClient()
@@ -851,12 +1039,20 @@ class BrightpearlService extends BaseService {
.filter((i) => !!i)
// Orders with a concatenated externalReference are swap orders
const [_, swapId] = order.externalRef.split(".")
const [_, partId] = order.externalRef.split(".")
if (swapId) {
return this.swapService_.createFulfillment(swapId, {
goods_out_note: id,
})
if (partId) {
if (partId.startsWith("claim")) {
return this.claimService_.createFulfillment(partId, {
goods_out_note: id,
})
}
if (partId.startsWith("swap")) {
return this.swapService_.createFulfillment(partId, {
goods_out_note: id,
})
}
}
return this.orderService_.createFulfillment(order.externalRef, lines, {
@@ -6,6 +6,7 @@ class OrderSubscriber {
returnService,
paymentProviderService,
brightpearlService,
claimService,
fulfillmentService,
}) {
this.orderService_ = orderService
@@ -14,9 +15,12 @@ class OrderSubscriber {
this.returnService_ = returnService
this.paymentProviderService_ = paymentProviderService
this.fulfillmentService_ = fulfillmentService
this.claimService_ = claimService
eventBusService.subscribe("order.placed", this.sendToBrightpearl)
eventBusService.subscribe("claim.created", this.registerClaim)
eventBusService.subscribe("order.refund_created", this.registerRefund)
eventBusService.subscribe("order.items_returned", this.registerReturn)
@@ -28,6 +32,7 @@ class OrderSubscriber {
eventBusService.subscribe("order.shipment_created", this.registerShipment)
eventBusService.subscribe("swap.shipment_created", this.registerShipment)
eventBusService.subscribe("claim.shipment_created", this.registerShipment)
// Before we initiate a swap we wait for the payment and the return
eventBusService.subscribe(
@@ -86,6 +91,34 @@ class OrderSubscriber {
await this.brightpearlService_.createSwapOrder(fromOrder, fromSwap)
}
registerClaim = async (data) => {
const { id } = data
const fromClaim = await this.claimService_.retrieve(id, {
relations: [
"order",
"order.payments",
"order.region",
"order.claims",
"order.discounts",
"claim_items",
"return_order",
"return_order.items",
"return_order.shipping_method",
"additional_items",
"shipping_address",
"shipping_methods",
],
})
const fromOrder = fromClaim.order
if (fromClaim.type === "replace") {
await this.brightpearlService_.createClaim(fromOrder, fromClaim)
} else if (fromClaim.type === "refund") {
await this.brightpearlService_.createClaimCredit(fromOrder, fromClaim)
}
}
registerShipment = async (data) => {
const { fulfillment_id } = data
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id)
@@ -301,6 +301,13 @@ class BrightpearlClient {
return this.buildSearchResults_(data.response)
})
},
retrievePrice: (productId) => {
return this.client_
.request({
url: `/product-service/product-price/${productId}`,
})
.then(({ data }) => data.response.length && data.response[0])
},
}
}
@@ -3,12 +3,55 @@ class OrderSubscriber {
segmentService,
eventBusService,
orderService,
claimService,
returnService,
}) {
this.orderService_ = orderService
this.returnService_ = returnService
this.claimService_ = claimService
eventBusService.subscribe("claim.created", async ({ id }) => {
const claim = await this.claimService_.retrieve(id, {
relations: [
"order",
"claim_items",
"claim_items.item",
"claim_items.tags",
"claim_items.variant",
],
})
for (const ci of claim.claim_items) {
const price = ci.item.unit_price / 100
const reporting_price = await segmentService.getReportingValue(
claim.order.currency_code,
price
)
const event = {
event: "Item Claimed",
userId: claim.order.customer_id,
timestamp: claim.created_at,
properties: {
price,
reporting_price,
order_id: claim.order_id,
claim_id: claim.id,
claim_item_id: ci.id,
type: claim.type,
quantity: ci.quantity,
variant: ci.variant.sku,
product_id: ci.variant.product_id,
reason: ci.reason,
note: ci.note,
tags: ci.tags.map((t) => ({ id: t.id, value: t.value })),
},
}
await segmentService.track(event)
}
})
eventBusService.subscribe(
"order.items_returned",
async ({ id, return_id }) => {
@@ -130,13 +173,12 @@ class OrderSubscriber {
],
})
const date = new Date(parseInt(order.created))
const orderData = await segmentService.buildOrder(order)
const orderEvent = {
event: "Order Completed",
userId: order.customer_id,
properties: orderData,
timestamp: date,
timestamp: order.created_at,
}
segmentService.track(orderEvent)
@@ -1,74 +1,93 @@
export default ({
create,
update,
remove,
softRemove,
find,
findOne,
findOneOrFail,
save,
findAndCount,
} = {}) => {
return {
create: jest.fn().mockImplementation((...args) => {
if (create) {
return create(...args);
}
return {};
}),
softRemove: jest.fn().mockImplementation((...args) => {
if (softRemove) {
return softRemove(...args);
}
return {};
}),
remove: jest.fn().mockImplementation((...args) => {
if (remove) {
return remove(...args);
}
return {};
}),
update: jest.fn().mockImplementation((...args) => {
if (update) {
return update(...args);
}
}),
findOneOrFail: jest.fn().mockImplementation((...args) => {
if (findOneOrFail) {
return findOneOrFail(...args);
}
}),
findOne: jest.fn().mockImplementation((...args) => {
if (findOne) {
return findOne(...args);
}
}),
findOneOrFail: jest.fn().mockImplementation((...args) => {
if (findOneOrFail) {
return findOneOrFail(...args);
}
}),
find: jest.fn().mockImplementation((...args) => {
if (find) {
return find(...args);
}
}),
softRemove: jest.fn().mockImplementation((...args) => {
if (softRemove) {
return softRemove(...args);
}
}),
save: jest.fn().mockImplementation((...args) => {
if (save) {
return save(...args);
}
return Promise.resolve(...args);
}),
findAndCount: jest.fn().mockImplementation((...args) => {
if (findAndCount) {
return findAndCount(...args);
}
return {};
}),
};
class MockRepo {
constructor({
create,
update,
remove,
softRemove,
find,
findOne,
findOneOrFail,
save,
findAndCount,
}) {
this.create_ = create;
this.update_ = update;
this.remove_ = remove;
this.softRemove_ = softRemove;
this.find_ = find;
this.findOne_ = findOne;
this.findOneOrFail_ = findOneOrFail;
this.save_ = save;
this.findAndCount_ = findAndCount;
}
setFindOne(fn) {
this.findOne_ = fn;
}
create = jest.fn().mockImplementation((...args) => {
if (this.create_) {
return this.create_(...args);
}
return {};
});
softRemove = jest.fn().mockImplementation((...args) => {
if (this.softRemove_) {
return this.softRemove_(...args);
}
return {};
});
remove = jest.fn().mockImplementation((...args) => {
if (this.remove_) {
return this.remove_(...args);
}
return {};
});
update = jest.fn().mockImplementation((...args) => {
if (this.update_) {
return this.update_(...args);
}
});
findOneOrFail = jest.fn().mockImplementation((...args) => {
if (this.findOneOrFail_) {
return this.findOneOrFail_(...args);
}
});
findOne = jest.fn().mockImplementation((...args) => {
if (this.findOne_) {
return this.findOne_(...args);
}
});
findOneOrFail = jest.fn().mockImplementation((...args) => {
if (this.findOneOrFail_) {
return this.findOneOrFail_(...args);
}
});
find = jest.fn().mockImplementation((...args) => {
if (this.find_) {
return this.find_(...args);
}
});
softRemove = jest.fn().mockImplementation((...args) => {
if (this.softRemove_) {
return this.softRemove_(...args);
}
});
save = jest.fn().mockImplementation((...args) => {
if (this.save_) {
return this.save_(...args);
}
return Promise.resolve(...args);
});
findAndCount = jest.fn().mockImplementation((...args) => {
if (this.findAndCount_) {
return this.findAndCount_(...args);
}
return {};
});
}
export default (methods = {}) => {
return new MockRepo(methods);
};
+53 -7
View File
@@ -971,6 +971,18 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@hapi/hoek@^9.0.0":
version "9.1.1"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa"
integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==
"@hapi/topo@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
dependencies:
"@hapi/hoek" "^9.0.0"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
@@ -1154,6 +1166,23 @@
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
"@sideway/address@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d"
integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==
dependencies:
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
"@sinonjs/commons@^1.7.0":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
@@ -3242,6 +3271,22 @@ jest@^25.5.2:
import-local "^3.0.2"
jest-cli "^25.5.2"
joi-objectid@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
joi@^17.3.0:
version "17.3.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
"@sideway/address" "^4.1.0"
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3470,17 +3515,18 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
<<<<<<< HEAD
math-random@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
=======
memory-pager@^1.0.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"
integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==
>>>>>>> master
medusa-core-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.0.tgz#0641b365b769dbf99856025d935eef5cf5d81f2c"
integrity sha512-zocRthKhLK3eSjrXbAhZZkIMBRxyvU7GcAMFh5UCEgfe7f935vjE7r5lGTr5jTEwgwaoTUk9ep0VBekz0SEdyw==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
merge-stream@^2.0.0:
version "2.0.0"
+3 -2
View File
@@ -2,7 +2,7 @@
"name": "@medusajs/medusa",
"version": "1.1.4",
"description": "E-commerce for JAMstack",
"main": "dist/app.js",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
@@ -39,7 +39,8 @@
"prepare": "cross-env NODE_ENV=production npm run build",
"build": "babel src -d dist --ignore **/__tests__ --extensions \".ts,.js\"",
"serve": "node dist/app.js",
"test": "jest"
"test": "jest",
"test:unit": "jest"
},
"peerDependencies": {
"medusa-interfaces": "1.x",
@@ -5,7 +5,7 @@ export default () => {
const logger = req.scope.resolve("logger")
logger.error(err.message)
console.log(err)
console.error(err)
let statusCode = 500
switch (err.name) {
@@ -13,6 +13,15 @@ const defaultRelations = [
"returns",
"gift_cards",
"gift_card_transactions",
"claims",
"claims.return_order",
"claims.shipping_methods",
"claims.shipping_address",
"claims.additional_items",
"claims.fulfillments",
"claims.claim_items",
"claims.claim_items.images",
"claims.claim_items.tags",
"swaps",
"swaps.return_order",
"swaps.payment",
@@ -0,0 +1,245 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
type: Validator.string()
.valid("replace", "refund")
.required(),
claim_items: Validator.array()
.items({
item_id: Validator.string().required(),
quantity: Validator.number().required(),
note: Validator.string().optional(),
reason: Validator.string().valid(
"missing_item",
"wrong_item",
"production_failure",
"other"
),
tags: Validator.array().items(Validator.string()),
images: Validator.array().items(Validator.string()),
})
.required(),
return_shipping: Validator.object()
.keys({
option_id: Validator.string().optional(),
price: Validator.number()
.integer()
.optional(),
})
.optional(),
additional_items: Validator.array()
.items({
variant_id: Validator.string().required(),
quantity: Validator.number().required(),
})
.optional(),
shipping_methods: Validator.array()
.items({
id: Validator.string().optional(),
option_id: Validator.string().optional(),
price: Validator.number()
.integer()
.optional(),
})
.optional(),
refund_amount: Validator.number()
.integer()
.optional(),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
const idempotencyKeyService = req.scope.resolve("idempotencyKeyService")
const headerKey = req.get("Idempotency-Key") || ""
let idempotencyKey
try {
idempotencyKey = await idempotencyKeyService.initializeRequest(
headerKey,
req.method,
req.params,
req.path
)
} catch (error) {
res.status(409).send("Failed to create idempotency key")
return
}
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
try {
const orderService = req.scope.resolve("orderService")
const claimService = req.scope.resolve("claimService")
const returnService = req.scope.resolve("returnService")
let inProgress = true
let err = false
while (inProgress) {
switch (idempotencyKey.recovery_point) {
case "started": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async manager => {
const order = await orderService
.withTransaction(manager)
.retrieve(id, {
relations: ["items", "discounts"],
})
await claimService.withTransaction(manager).create({
idempotency_key: idempotencyKey.idempotency_key,
order,
type: value.type,
claim_items: value.claim_items,
return_shipping: value.return_shipping,
additional_items: value.additional_items,
shipping_methods: value.shipping_methods,
metadata: value.metadata,
})
return {
recovery_point: "claim_created",
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
break
}
case "claim_created": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async manager => {
let claim = await claimService.withTransaction(manager).list({
idempotency_key: idempotencyKey.idempotency_key,
})
if (!claim.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claim not found`
)
}
claim = claim[0]
if (claim.type === "refund") {
await claimService
.withTransaction(manager)
.processRefund(claim.id)
}
return {
recovery_point: "refund_handled",
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
break
}
case "refund_handled": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async manager => {
let order = await orderService
.withTransaction(manager)
.retrieve(id, {
relations: ["items", "discounts"],
})
let claim = await claimService.withTransaction(manager).list(
{
idempotency_key: idempotencyKey.idempotency_key,
},
{
relations: ["return_order"],
}
)
if (!claim.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claim not found`
)
}
claim = claim[0]
if (claim.return_order) {
await returnService
.withTransaction(manager)
.fulfill(claim.return_order.id)
}
order = await orderService.withTransaction(manager).retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
return {
response_code: 200,
response_body: { order },
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
break
}
case "finished": {
inProgress = false
break
}
default:
idempotencyKey = await idempotencyKeyService.update(
idempotencyKey.idempotency_key,
{
recovery_point: "finished",
response_code: 500,
response_body: { message: "Unknown recovery point" },
}
)
break
}
}
if (err) {
throw err
}
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
} catch (error) {
throw error
}
}
@@ -0,0 +1,36 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
export default async (req, res) => {
const { id, claim_id } = req.params
const schema = Validator.object().keys({
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const orderService = req.scope.resolve("orderService")
const claimService = req.scope.resolve("claimService")
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
await claimService
.withTransaction(manager)
.createFulfillment(claim_id, value.metadata)
})
const order = await orderService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ order })
} catch (err) {
throw err
}
}
@@ -140,6 +140,27 @@ export default app => {
middlewares.wrap(require("./process-swap-payment").default)
)
/**
* Creates a claim
*/
route.post("/:id/claims", middlewares.wrap(require("./create-claim").default))
/**
* Updates a claim
*/
route.post(
"/:id/claims/:claim_id",
middlewares.wrap(require("./update-claim").default)
)
/**
* Creates claim fulfillment
*/
route.post(
"/:id/claims/:claim_id/fulfillments",
middlewares.wrap(require("./fulfill-claim").default)
)
/**
* Delete metadata key / value pair.
*/
@@ -162,6 +183,15 @@ export const defaultRelations = [
"returns",
"gift_cards",
"gift_card_transactions",
"claims",
"claims.return_order",
"claims.shipping_methods",
"claims.shipping_address",
"claims.additional_items",
"claims.fulfillments",
"claims.claim_items",
"claims.claim_items.images",
"claims.claim_items.tags",
"swaps",
"swaps.return_order",
"swaps.payment",
@@ -234,6 +264,7 @@ export const allowedRelations = [
"payments",
"fulfillments",
"returns",
"claims",
"swaps",
"swaps.return_order",
"swaps.additional_items",
@@ -0,0 +1,56 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
export default async (req, res) => {
const { id, claim_id } = req.params
const schema = Validator.object().keys({
claim_items: Validator.array()
.items({
id: Validator.string().required(),
note: Validator.string().allow(null, ""),
reason: Validator.string().allow(null, ""),
images: Validator.array().items({
id: Validator.string().optional(),
url: Validator.string().optional(),
}),
tags: Validator.array().items({
id: Validator.string().optional(),
value: Validator.string().optional(),
}),
metadata: Validator.object().optional(),
})
.optional(),
shipping_methods: Validator.array()
.items({
id: Validator.string().optional(),
option_id: Validator.string().optional(),
price: Validator.number()
.integer()
.optional(),
})
.optional(),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const orderService = req.scope.resolve("orderService")
const claimService = req.scope.resolve("claimService")
await claimService.update(claim_id, value)
const data = await orderService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.json({ order: data })
} catch (error) {
throw error
}
}
-24
View File
@@ -1,24 +0,0 @@
import "core-js/stable"
import "regenerator-runtime/runtime"
import "reflect-metadata"
import express from "express"
import loaders from "./loaders"
import Logger from "./loaders/logger"
const PORT = process.env.PORT || 80
const startServer = async () => {
const app = express()
await loaders({ expressApp: app })
app.listen(PORT, err => {
if (err) {
console.log(err)
return
}
Logger.info(`Server is ready on port: ${PORT}!`)
})
}
startServer()
+39
View File
@@ -0,0 +1,39 @@
export { Address } from "./models/address"
export { Cart } from "./models/cart"
export { ClaimImage } from "./models/claim-image"
export { ClaimItem } from "./models/claim-item"
export { ClaimTag } from "./models/claim-tag"
export { Country } from "./models/country"
export { Currency } from "./models/currency"
export { Customer } from "./models/customer"
export { DiscountRule } from "./models/discount-rule"
export { Discount } from "./models/discount"
export { FulfillmentItem } from "./models/fulfillment-item"
export { FulfillmentProvider } from "./models/fulfillment-provider"
export { GiftCardTransaction } from "./models/gift-card-transaction"
export { GiftCard } from "./models/gift-card"
export { IdempotencyKey } from "./models/idempotency-key"
export { Image } from "./models/image"
export { LineItem } from "./models/line-item"
export { MoneyAmount } from "./models/money-amount"
export { Oauth } from "./models/oauth"
export { Order } from "./models/order"
export { PaymentProvider } from "./models/payment-provider"
export { PaymentSession } from "./models/payment-session"
export { Payment } from "./models/payment"
export { ProductOptionValue } from "./models/product-option-value"
export { ProductOption } from "./models/product-option"
export { ProductVariant } from "./models/product-variant"
export { Product } from "./models/product"
export { Refund } from "./models/refund"
export { Region } from "./models/region"
export { ReturnItem } from "./models/return-item"
export { Return } from "./models/return"
export { ShippingMethod } from "./models/shipping-method"
export { ShippingOptionRequirement } from "./models/shipping-option-requirement"
export { ShippingOption } from "./models/shipping-option"
export { ShippingProfile } from "./models/shipping-profile"
export { StagedJob } from "./models/staged-job"
export { Store } from "./models/store"
export { Swap } from "./models/swap"
export { User } from "./models/user"
+14 -6
View File
@@ -7,25 +7,33 @@ import { Lifetime, asClass, asValue } from "awilix"
/**
* Registers all models in the model directory
*/
export default ({ container }) => {
export default ({ container }, config = { register: true }) => {
let corePath = "../models/*.js"
const coreFull = path.join(__dirname, corePath)
const toReturn = []
const core = glob.sync(coreFull, { cwd: __dirname })
core.forEach(fn => {
const loaded = require(fn)
Object.entries(loaded).map(([key, val]) => {
if (typeof val === "function" || val instanceof EntitySchema) {
const name = formatRegistrationName(fn)
container.register({
[name]: asClass(val),
})
if (config.register) {
const name = formatRegistrationName(fn)
container.register({
[name]: asClass(val),
})
container.registerAdd("db_entities", asValue(val))
container.registerAdd("db_entities", asValue(val))
}
toReturn.push(val)
}
})
})
return toReturn
}
function formatRegistrationName(fn) {
@@ -0,0 +1,213 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class claims1612284947120 implements MigrationInterface {
name = "claims1612284947120"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "shipping_method" DROP CONSTRAINT "CHK_3c00b878c1426d119cd70aa065"`
)
await queryRunner.query(
`CREATE TABLE "claim_image" ("id" character varying NOT NULL, "claim_item_id" character varying NOT NULL, "url" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_7c49e44bfe8840ca7d917890101" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE TABLE "claim_tag" ("id" character varying NOT NULL, "value" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_7761180541142a5926501018d34" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE INDEX "IDX_ec10c54769877840c132260e4a" ON "claim_tag" ("value") `
)
await queryRunner.query(
`CREATE TYPE "claim_item_reason_enum" AS ENUM('missing_item', 'wrong_item', 'production_failure', 'other')`
)
await queryRunner.query(
`CREATE TABLE "claim_item" ("id" character varying NOT NULL, "claim_order_id" character varying NOT NULL, "item_id" character varying NOT NULL, "variant_id" character varying NOT NULL, "reason" "claim_item_reason_enum" NOT NULL, "note" character varying, "quantity" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_5679662039bc4c7c6bc7fa1be2d" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE INDEX "IDX_900a9c3834257304396b2b0fe7" ON "claim_item" ("claim_order_id") `
)
await queryRunner.query(
`CREATE INDEX "IDX_6e0cad0daef76bb642675910b9" ON "claim_item" ("item_id") `
)
await queryRunner.query(
`CREATE INDEX "IDX_64980511ca32c8e92b417644af" ON "claim_item" ("variant_id") `
)
await queryRunner.query(
`CREATE TYPE "claim_order_payment_status_enum" AS ENUM('na', 'not_refunded', 'refunded')`
)
await queryRunner.query(
`CREATE TYPE "claim_order_fulfillment_status_enum" AS ENUM('not_fulfilled', 'partially_fulfilled', 'fulfilled', 'partially_shipped', 'shipped', 'partially_returned', 'returned', 'canceled', 'requires_action')`
)
await queryRunner.query(
`CREATE TYPE "claim_order_type_enum" AS ENUM('refund', 'replace')`
)
await queryRunner.query(
`CREATE TABLE "claim_order" ("id" character varying NOT NULL, "payment_status" "claim_order_payment_status_enum" NOT NULL DEFAULT 'na', "fulfillment_status" "claim_order_fulfillment_status_enum" NOT NULL DEFAULT 'not_fulfilled', "type" "claim_order_type_enum" NOT NULL, "order_id" character varying NOT NULL, "shipping_address_id" character varying, "refund_amount" integer, "canceled_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "PK_8981f5595a4424021466aa4c7a4" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE TABLE "claim_item_tags" ("item_id" character varying NOT NULL, "tag_id" character varying NOT NULL, CONSTRAINT "PK_54ab8ce0f7e99167068188fbd81" PRIMARY KEY ("item_id", "tag_id"))`
)
await queryRunner.query(
`CREATE INDEX "IDX_c2c0f3edf39515bd15432afe6e" ON "claim_item_tags" ("item_id") `
)
await queryRunner.query(
`CREATE INDEX "IDX_dc9bbf9fcb9ba458d25d512811" ON "claim_item_tags" ("tag_id") `
)
await queryRunner.query(
`ALTER TABLE "shipping_method" ADD "claim_order_id" character varying`
)
await queryRunner.query(
`ALTER TABLE "return" ADD "claim_order_id" character varying`
)
await queryRunner.query(
`ALTER TABLE "return" ADD CONSTRAINT "UQ_71773d56eb2bacb922bc3283398" UNIQUE ("claim_order_id")`
)
await queryRunner.query(
`ALTER TABLE "fulfillment" ADD "claim_order_id" character varying`
)
await queryRunner.query(
`ALTER TABLE "line_item" ADD "claim_order_id" character varying`
)
await queryRunner.query(
`ALTER TYPE "public"."refund_reason_enum" RENAME TO "refund_reason_enum_old"`
)
await queryRunner.query(
`CREATE TYPE "refund_reason_enum" AS ENUM('discount', 'return', 'swap', 'claim', 'other')`
)
await queryRunner.query(
`ALTER TABLE "refund" ALTER COLUMN "reason" TYPE "refund_reason_enum" USING "reason"::"text"::"refund_reason_enum"`
)
await queryRunner.query(`DROP TYPE "refund_reason_enum_old"`)
await queryRunner.query(`COMMENT ON COLUMN "refund"."reason" IS NULL`)
await queryRunner.query(
`CREATE INDEX "IDX_d783a66d1c91c0858752c933e6" ON "shipping_method" ("claim_order_id") `
)
await queryRunner.query(
`CREATE INDEX "IDX_118e3c48f09a7728f41023c94e" ON "line_item" ("claim_order_id") `
)
await queryRunner.query(
`ALTER TABLE "shipping_method" ADD CONSTRAINT "CHK_a7020b08665bbd64d84a6641cf" CHECK ("claim_order_id" IS NOT NULL OR "order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL)`
)
await queryRunner.query(
`ALTER TABLE "claim_image" ADD CONSTRAINT "FK_21cbfedd83d736d86f4c6f4ce56" FOREIGN KEY ("claim_item_id") REFERENCES "claim_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_item" ADD CONSTRAINT "FK_900a9c3834257304396b2b0fe7c" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_item" ADD CONSTRAINT "FK_6e0cad0daef76bb642675910b9d" FOREIGN KEY ("item_id") REFERENCES "line_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_item" ADD CONSTRAINT "FK_64980511ca32c8e92b417644afa" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_d783a66d1c91c0858752c933e68" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "return" ADD CONSTRAINT "FK_71773d56eb2bacb922bc3283398" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_order" ADD CONSTRAINT "FK_f49e3974465d3c3a33d449d3f31" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_order" ADD CONSTRAINT "FK_017d58bf8260c6e1a2588d258e2" FOREIGN KEY ("shipping_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "fulfillment" ADD CONSTRAINT "FK_d73e55964e0ff2db8f03807d52e" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "line_item" ADD CONSTRAINT "FK_118e3c48f09a7728f41023c94ef" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_item_tags" ADD CONSTRAINT "FK_c2c0f3edf39515bd15432afe6e5" FOREIGN KEY ("item_id") REFERENCES "claim_item"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "claim_item_tags" ADD CONSTRAINT "FK_dc9bbf9fcb9ba458d25d512811b" FOREIGN KEY ("tag_id") REFERENCES "claim_tag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "claim_item_tags" DROP CONSTRAINT "FK_dc9bbf9fcb9ba458d25d512811b"`
)
await queryRunner.query(
`ALTER TABLE "claim_item_tags" DROP CONSTRAINT "FK_c2c0f3edf39515bd15432afe6e5"`
)
await queryRunner.query(
`ALTER TABLE "line_item" DROP CONSTRAINT "FK_118e3c48f09a7728f41023c94ef"`
)
await queryRunner.query(
`ALTER TABLE "fulfillment" DROP CONSTRAINT "FK_d73e55964e0ff2db8f03807d52e"`
)
await queryRunner.query(
`ALTER TABLE "claim_order" DROP CONSTRAINT "FK_017d58bf8260c6e1a2588d258e2"`
)
await queryRunner.query(
`ALTER TABLE "claim_order" DROP CONSTRAINT "FK_f49e3974465d3c3a33d449d3f31"`
)
await queryRunner.query(
`ALTER TABLE "return" DROP CONSTRAINT "FK_71773d56eb2bacb922bc3283398"`
)
await queryRunner.query(
`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_d783a66d1c91c0858752c933e68"`
)
await queryRunner.query(
`ALTER TABLE "claim_item" DROP CONSTRAINT "FK_64980511ca32c8e92b417644afa"`
)
await queryRunner.query(
`ALTER TABLE "claim_item" DROP CONSTRAINT "FK_6e0cad0daef76bb642675910b9d"`
)
await queryRunner.query(
`ALTER TABLE "claim_item" DROP CONSTRAINT "FK_900a9c3834257304396b2b0fe7c"`
)
await queryRunner.query(
`ALTER TABLE "claim_image" DROP CONSTRAINT "FK_21cbfedd83d736d86f4c6f4ce56"`
)
await queryRunner.query(
`ALTER TABLE "shipping_method" DROP CONSTRAINT "CHK_a7020b08665bbd64d84a6641cf"`
)
await queryRunner.query(`DROP INDEX "IDX_118e3c48f09a7728f41023c94e"`)
await queryRunner.query(`DROP INDEX "IDX_d783a66d1c91c0858752c933e6"`)
await queryRunner.query(`COMMENT ON COLUMN "refund"."reason" IS NULL`)
await queryRunner.query(
`CREATE TYPE "refund_reason_enum_old" AS ENUM('discount', 'return', 'swap', 'other')`
)
await queryRunner.query(
`ALTER TABLE "refund" ALTER COLUMN "reason" TYPE "refund_reason_enum_old" USING "reason"::"text"::"refund_reason_enum_old"`
)
await queryRunner.query(`DROP TYPE "refund_reason_enum"`)
await queryRunner.query(
`ALTER TYPE "refund_reason_enum_old" RENAME TO "refund_reason_enum"`
)
await queryRunner.query(
`ALTER TABLE "line_item" DROP COLUMN "claim_order_id"`
)
await queryRunner.query(
`ALTER TABLE "fulfillment" DROP COLUMN "claim_order_id"`
)
await queryRunner.query(
`ALTER TABLE "return" DROP CONSTRAINT "UQ_71773d56eb2bacb922bc3283398"`
)
await queryRunner.query(`ALTER TABLE "return" DROP COLUMN "claim_order_id"`)
await queryRunner.query(
`ALTER TABLE "shipping_method" DROP COLUMN "claim_order_id"`
)
await queryRunner.query(`DROP INDEX "IDX_dc9bbf9fcb9ba458d25d512811"`)
await queryRunner.query(`DROP INDEX "IDX_c2c0f3edf39515bd15432afe6e"`)
await queryRunner.query(`DROP TABLE "claim_item_tags"`)
await queryRunner.query(`DROP TABLE "claim_order"`)
await queryRunner.query(`DROP TYPE "claim_order_type_enum"`)
await queryRunner.query(`DROP TYPE "claim_order_fulfillment_status_enum"`)
await queryRunner.query(`DROP TYPE "claim_order_payment_status_enum"`)
await queryRunner.query(`DROP INDEX "IDX_64980511ca32c8e92b417644af"`)
await queryRunner.query(`DROP INDEX "IDX_6e0cad0daef76bb642675910b9"`)
await queryRunner.query(`DROP INDEX "IDX_900a9c3834257304396b2b0fe7"`)
await queryRunner.query(`DROP TABLE "claim_item"`)
await queryRunner.query(`DROP TYPE "claim_item_reason_enum"`)
await queryRunner.query(`DROP INDEX "IDX_ec10c54769877840c132260e4a"`)
await queryRunner.query(`DROP TABLE "claim_tag"`)
await queryRunner.query(`DROP TABLE "claim_image"`)
await queryRunner.query(
`ALTER TABLE "shipping_method" ADD CONSTRAINT "CHK_3c00b878c1426d119cd70aa065" CHECK (((order_id IS NOT NULL) OR (cart_id IS NOT NULL) OR (swap_id IS NOT NULL) OR (return_id IS NOT NULL)))`
)
}
}
+52
View File
@@ -0,0 +1,52 @@
import {
Entity,
BeforeInsert,
DeleteDateColumn,
CreateDateColumn,
UpdateDateColumn,
Column,
PrimaryColumn,
ManyToOne,
JoinColumn,
} from "typeorm"
import { ulid } from "ulid"
import { ClaimItem } from "./claim-item"
@Entity()
export class ClaimImage {
@PrimaryColumn()
id: string
@Column()
claim_item_id: string
@ManyToOne(
() => ClaimItem,
ci => ci.images
)
@JoinColumn({ name: "claim_item_id" })
claim_item: ClaimItem
@Column()
url: string
@CreateDateColumn({ type: "timestamptz" })
created_at: Date
@UpdateDateColumn({ type: "timestamptz" })
updated_at: Date
@DeleteDateColumn({ type: "timestamptz" })
deleted_at: Date
@Column({ type: "jsonb", nullable: true })
metadata: any
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `cimg_${id}`
}
}
+111
View File
@@ -0,0 +1,111 @@
import {
Entity,
BeforeInsert,
DeleteDateColumn,
CreateDateColumn,
UpdateDateColumn,
Column,
PrimaryColumn,
Index,
ManyToMany,
ManyToOne,
OneToMany,
JoinColumn,
JoinTable,
} from "typeorm"
import { ulid } from "ulid"
import { LineItem } from "./line-item"
import { ClaimImage } from "./claim-image"
import { ClaimTag } from "./claim-tag"
import { ClaimOrder } from "./claim-order"
import { ProductVariant } from "./product-variant"
export enum ClaimReason {
MISSING_ITEM = "missing_item",
WRONG_ITEM = "wrong_item",
PRODUCTION_FAILURE = "production_failure",
OTHER = "other",
}
@Entity()
export class ClaimItem {
@PrimaryColumn()
id: string
@OneToMany(
() => ClaimImage,
ci => ci.claim_item,
{ cascade: ["insert", "remove"] }
)
images: ClaimImage[]
@Index()
@Column()
claim_order_id: string
@ManyToOne(
() => ClaimOrder,
co => co.claim_items
)
@JoinColumn({ name: "claim_order_id" })
claim_order: ClaimOrder
@Index()
@Column()
item_id: string
@ManyToOne(() => LineItem)
@JoinColumn({ name: "item_id" })
item: LineItem
@Index()
@Column()
variant_id: string
@ManyToOne(() => ProductVariant)
@JoinColumn({ name: "variant_id" })
variant: ProductVariant
@Column({ type: "enum", enum: ClaimReason })
reason: ClaimReason
@Column({ nullable: true })
note: string
@Column({ type: "int" })
quantity: number
@ManyToMany(() => ClaimTag, { cascade: ["insert"] })
@JoinTable({
name: "claim_item_tags",
joinColumn: {
name: "item_id",
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "tag_id",
referencedColumnName: "id",
},
})
tags: ClaimTag[]
@CreateDateColumn({ type: "timestamptz" })
created_at: Date
@UpdateDateColumn({ type: "timestamptz" })
updated_at: Date
@DeleteDateColumn({ type: "timestamptz" })
deleted_at: Date
@Column({ type: "jsonb", nullable: true })
metadata: any
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `citm_${id}`
}
}
+147
View File
@@ -0,0 +1,147 @@
import {
Entity,
BeforeInsert,
DeleteDateColumn,
CreateDateColumn,
UpdateDateColumn,
Column,
PrimaryColumn,
Index,
OneToOne,
ManyToOne,
OneToMany,
JoinColumn,
} from "typeorm"
import { ulid } from "ulid"
import { Fulfillment } from "./fulfillment"
import { LineItem } from "./line-item"
import { ClaimItem } from "./claim-item"
import { Order } from "./order"
import { Return } from "./return"
import { ShippingMethod } from "./shipping-method"
import { Address } from "./address"
export enum ClaimType {
REFUND = "refund",
REPLACE = "replace",
}
export enum ClaimPaymentStatus {
NA = "na",
NOT_REFUNDED = "not_refunded",
REFUNDED = "refunded",
}
export enum ClaimFulfillmentStatus {
NOT_FULFILLED = "not_fulfilled",
PARTIALLY_FULFILLED = "partially_fulfilled",
FULFILLED = "fulfilled",
PARTIALLY_SHIPPED = "partially_shipped",
SHIPPED = "shipped",
PARTIALLY_RETURNED = "partially_returned",
RETURNED = "returned",
CANCELED = "canceled",
REQUIRES_ACTION = "requires_action",
}
@Entity()
export class ClaimOrder {
@PrimaryColumn()
id: string
@Column({
type: "enum",
enum: ClaimPaymentStatus,
default: ClaimPaymentStatus.NA,
})
payment_status: ClaimPaymentStatus
@Column({
type: "enum",
enum: ClaimFulfillmentStatus,
default: ClaimFulfillmentStatus.NOT_FULFILLED,
})
fulfillment_status: ClaimFulfillmentStatus
@OneToMany(
() => ClaimItem,
ci => ci.claim_order
)
claim_items: ClaimItem[]
@OneToMany(
() => LineItem,
li => li.claim_order,
{ cascade: ["insert"] }
)
additional_items: LineItem[]
@Column({ type: "enum", enum: ClaimType })
type: ClaimType
@Column()
order_id: string
@ManyToOne(
() => Order,
o => o.claims
)
@JoinColumn({ name: "order_id" })
order: Order
@OneToOne(
() => Return,
ret => ret.claim_order
)
return_order: Return
@Column({ nullable: true })
shipping_address_id: string
@ManyToOne(() => Address, { cascade: ["insert"] })
@JoinColumn({ name: "shipping_address_id" })
shipping_address: Address
@OneToMany(
() => ShippingMethod,
method => method.claim_order,
{ cascade: ["insert"] }
)
shipping_methods: ShippingMethod[]
@OneToMany(
() => Fulfillment,
fulfillment => fulfillment.claim_order,
{ cascade: ["insert"] }
)
fulfillments: Fulfillment[]
@Column({ type: "int", nullable: true })
refund_amount: number
@Column({ type: "timestamptz", nullable: true })
canceled_at: Date
@CreateDateColumn({ type: "timestamptz" })
created_at: Date
@UpdateDateColumn({ type: "timestamptz" })
updated_at: Date
@DeleteDateColumn({ type: "timestamptz" })
deleted_at: Date
@Column({ type: "jsonb", nullable: true })
metadata: any
@Column({ nullable: true })
idempotency_key: string
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `claim_${id}`
}
}
+43
View File
@@ -0,0 +1,43 @@
import {
Entity,
BeforeInsert,
DeleteDateColumn,
CreateDateColumn,
UpdateDateColumn,
Column,
PrimaryColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from "typeorm"
import { ulid } from "ulid"
@Entity()
export class ClaimTag {
@PrimaryColumn()
id: string
@Index()
@Column()
value: string
@CreateDateColumn({ type: "timestamptz" })
created_at: Date
@UpdateDateColumn({ type: "timestamptz" })
updated_at: Date
@DeleteDateColumn({ type: "timestamptz" })
deleted_at: Date
@Column({ type: "jsonb", nullable: true })
metadata: any
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `ctag_${id}`
}
}
+11
View File
@@ -20,12 +20,23 @@ import { Order } from "./order"
import { FulfillmentProvider } from "./fulfillment-provider"
import { FulfillmentItem } from "./fulfillment-item"
import { Swap } from "./swap"
import { ClaimOrder } from "./claim-order"
@Entity()
export class Fulfillment {
@PrimaryColumn()
id: string
@Column({ nullable: true })
claim_order_id: string
@ManyToOne(
() => ClaimOrder,
co => co.fulfillments
)
@JoinColumn({ name: "claim_order_id" })
claim_order: ClaimOrder
@Column({ nullable: true })
swap_id: string
+12
View File
@@ -15,6 +15,7 @@ import { ulid } from "ulid"
import { Swap } from "./swap"
import { Cart } from "./cart"
import { Order } from "./order"
import { ClaimOrder } from "./claim-order"
import { ProductVariant } from "./product-variant"
@Check(`"fulfilled_quantity" <= "quantity"`)
@@ -59,6 +60,17 @@ export class LineItem {
@JoinColumn({ name: "swap_id" })
swap: Swap
@Index()
@Column({ nullable: true })
claim_order_id: string
@ManyToOne(
() => ClaimOrder,
co => co.additional_items
)
@JoinColumn({ name: "claim_order_id" })
claim_order: ClaimOrder
@Column()
title: string
+8
View File
@@ -29,6 +29,7 @@ import { Fulfillment } from "./fulfillment"
import { Return } from "./return"
import { Refund } from "./refund"
import { Swap } from "./swap"
import { ClaimOrder } from "./claim-order"
import { ShippingMethod } from "./shipping-method"
export enum OrderStatus {
@@ -183,6 +184,13 @@ export class Order {
)
returns: Return[]
@OneToMany(
() => ClaimOrder,
co => co.order,
{ cascade: ["insert"] }
)
claims: ClaimOrder[]
@OneToMany(
() => Refund,
ref => ref.order,
+1
View File
@@ -19,6 +19,7 @@ export enum RefundReason {
DISCOUNT = "discount",
RETURN = "return",
SWAP = "swap",
CLAIM = "claim",
OTHER = "other",
}
+11
View File
@@ -18,6 +18,7 @@ import { ulid } from "ulid"
import { Order } from "./order"
import { Swap } from "./swap"
import { ClaimOrder } from "./claim-order"
import { ReturnItem } from "./return-item"
import { ShippingMethod } from "./shipping-method"
@@ -52,6 +53,16 @@ export class Return {
@JoinColumn({ name: "swap_id" })
swap: Swap
@Column({ nullable: true })
claim_order_id: string
@OneToOne(
() => ClaimOrder,
co => co.return_order
)
@JoinColumn({ name: "claim_order_id" })
claim_order: ClaimOrder
@Column({ nullable: true })
order_id: string
@@ -1,12 +0,0 @@
/*******************************************************************************
* models/money-amount.js
*
******************************************************************************/
import mongoose from "mongoose"
export default new mongoose.Schema({
region_id: { type: String },
currency_code: { type: String, required: true },
amount: { type: Number, required: true, min: 0 },
sale_amount: { type: Number, min: 0 },
})
+10 -1
View File
@@ -11,6 +11,7 @@ import {
} from "typeorm"
import { ulid } from "ulid"
import { ClaimOrder } from "./claim-order"
import { Order } from "./order"
import { Cart } from "./cart"
import { Swap } from "./swap"
@@ -18,7 +19,7 @@ import { Return } from "./return"
import { ShippingOption } from "./shipping-option"
@Check(
`"order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL`
`"claim_order_id" IS NOT NULL OR "order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL`
)
@Check(`"price" >= 0`)
@Entity()
@@ -38,6 +39,14 @@ export class ShippingMethod {
@JoinColumn({ name: "order_id" })
order: Order
@Index()
@Column({ nullable: true })
claim_order_id: string
@ManyToOne(() => ClaimOrder)
@JoinColumn({ name: "claim_order_id" })
claim_order: ClaimOrder
@Index()
@Column({ nullable: true })
cart_id: string
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { ClaimImage } from "../models/claim-image"
@EntityRepository(ClaimImage)
export class ClaimImageRepository extends Repository<ClaimImage> {}
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { ClaimItem } from "../models/claim-item"
@EntityRepository(ClaimItem)
export class ClaimItemRepository extends Repository<ClaimItem> {}
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { ClaimTag } from "../models/claim-tag"
@EntityRepository(ClaimTag)
export class ClaimTagRepository extends Repository<ClaimTag> {}
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { ClaimOrder } from "../models/claim-order"
@EntityRepository(ClaimOrder)
export class ClaimRepository extends Repository<ClaimOrder> {}
@@ -0,0 +1,128 @@
import _ from "lodash"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import ClaimItemService from "../claim-item"
const withTransactionMock = jest.fn()
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
withTransactionMock("eventBus")
return this
},
}
describe("ClaimItemService", () => {
describe("create", () => {
const testItem = {
claim_order_id: "claim_13",
item_id: "itm_1",
tags: ["fluff"],
reason: "production_failure",
note: "Details",
quantity: 1,
images: ["url.com/1234"],
}
const claimTagRepo = MockRepository({
findOne: () => Promise.resolve(),
create: d => d,
})
const claimImgRepo = MockRepository({
findOne: () => Promise.resolve(),
create: d => d,
})
const claimItemRepo = MockRepository({
create: d => ({ id: "ci_1234", ...d }),
})
const lineItemService = {
withTransaction: function() {
withTransactionMock("lineItem")
return this
},
}
const claimItemService = new ClaimItemService({
manager: MockManager,
lineItemService,
claimTagRepository: claimTagRepo,
claimItemRepository: claimItemRepo,
claimImageRepository: claimImgRepo,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully creates a claim item", async () => {
lineItemService.retrieve = jest.fn(() =>
Promise.resolve({ fulfilled_quantity: 1 })
)
await claimItemService.create(testItem)
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
expect(lineItemService.retrieve).toHaveBeenCalledTimes(1)
expect(claimTagRepo.findOne).toHaveBeenCalledTimes(1)
expect(claimTagRepo.findOne).toHaveBeenCalledWith({
where: { value: "fluff" },
})
expect(claimTagRepo.create).toHaveBeenCalledTimes(1)
expect(claimTagRepo.create).toHaveBeenCalledWith({
value: "fluff",
})
expect(claimItemRepo.create).toHaveBeenCalledTimes(1)
expect(claimItemRepo.create).toHaveBeenCalledWith({
claim_order_id: "claim_13",
item_id: "itm_1",
reason: "production_failure",
note: "Details",
quantity: 1,
tags: [{ value: "fluff" }],
images: [{ url: "url.com/1234" }],
})
})
it("normalizes claim tag value", async () => {
lineItemService.retrieve = jest.fn(() =>
Promise.resolve({ fulfilled_quantity: 1 })
)
await claimItemService.create({
...testItem,
tags: [" FLUFF "],
})
expect(claimTagRepo.findOne).toHaveBeenCalledTimes(1)
expect(claimTagRepo.findOne).toHaveBeenCalledWith({
where: { value: "fluff" },
})
})
it("fails if fulfilled_quantity < quantity", async () => {
lineItemService.retrieve = jest.fn(() =>
Promise.resolve({ fulfilled_quantity: 0 })
)
await expect(claimItemService.create(testItem)).rejects.toThrow(
"Cannot claim more of an item than has been fulfilled"
)
})
it("fails if reason is unknown", async () => {
lineItemService.retrieve = jest.fn(() =>
Promise.resolve({ fulfilled_quantity: 1 })
)
await expect(
claimItemService.create({
...testItem,
reason: "unknown",
})
).rejects.toThrow(
`Claim Item reason must be one of "missing_item", "wrong_item", "production_failure" or "other".`
)
})
})
})
@@ -0,0 +1,539 @@
import _ from "lodash"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import ClaimService from "../claim"
const withTransactionMock = jest.fn()
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
withTransactionMock("eventBus")
return this
},
}
const totalsService = {
getRefundTotal: jest.fn(() => 1000),
}
describe("ClaimService", () => {
describe("create", () => {
const testClaim = {
type: "refund",
order: {
id: "1234",
region_id: "order_region",
items: [
{
id: "itm_1",
unit_price: 8000,
},
],
},
claim_items: [
{
item_id: "itm_1",
tags: ["fluff"],
reason: "production_failure",
note: "Details",
quantity: 1,
images: ["url.com/1234"],
},
],
return_shipping: {
option_id: "opt_13",
price: 0,
},
additional_items: [
{
variant_id: "var_123",
quantity: 1,
},
],
shipping_address_id: "adr_1234",
}
const claimRepo = MockRepository({
create: d => ({ id: "claim_134", ...d }),
})
const returnService = {
create: jest.fn(),
withTransaction: function() {
withTransactionMock("return")
return this
},
}
const lineItemService = {
generate: jest.fn((d, _, q) => ({ variant_id: d, quantity: q })),
withTransaction: function() {
withTransactionMock("lineItem")
return this
},
}
const claimItemService = {
create: jest.fn(),
withTransaction: function() {
withTransactionMock("claimItem")
return this
},
}
const claimService = new ClaimService({
manager: MockManager,
claimRepository: claimRepo,
totalsService,
returnService,
lineItemService,
claimItemService,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully creates a claim", async () => {
await claimService.create(testClaim)
expect(withTransactionMock).toHaveBeenCalledWith("return")
expect(returnService.create).toHaveBeenCalledTimes(1)
expect(returnService.create).toHaveBeenCalledWith(
{
claim_order_id: "claim_134",
shipping_method: {
option_id: "opt_13",
price: 0,
},
items: [
{
item_id: "itm_1",
quantity: 1,
},
],
},
{
id: "1234",
region_id: "order_region",
items: [
{
id: "itm_1",
unit_price: 8000,
},
],
}
)
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
expect(lineItemService.generate).toHaveBeenCalledWith(
"var_123",
"order_region",
1
)
expect(withTransactionMock).toHaveBeenCalledWith("claimItem")
expect(claimItemService.create).toHaveBeenCalledTimes(1)
expect(claimItemService.create).toHaveBeenCalledWith({
claim_order_id: "claim_134",
item_id: "itm_1",
tags: ["fluff"],
reason: "production_failure",
note: "Details",
quantity: 1,
images: ["url.com/1234"],
})
expect(claimRepo.create).toHaveBeenCalledTimes(1)
expect(claimRepo.create).toHaveBeenCalledWith({
payment_status: "not_refunded",
refund_amount: 1000,
type: "refund",
order_id: "1234",
additional_items: [
{
variant_id: "var_123",
quantity: 1,
},
],
shipping_address_id: "adr_1234",
})
expect(claimRepo.save).toHaveBeenCalledTimes(1)
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith("claim.created", {
id: "claim_134",
})
})
it("return only created when return shipping provided", async () => {
await claimService.create({
...testClaim,
return_shipping: null,
})
expect(returnService.create).toHaveBeenCalledTimes(0)
})
it("fails if replace and no additional items", async () => {
await expect(
claimService.create({
...testClaim,
type: "replace",
additional_items: null,
})
).rejects.toThrow(
`Claims with type "replace" must have at least one additional item.`
)
})
it("fails if replace and refund amount", async () => {
await expect(
claimService.create({
...testClaim,
type: "replace",
refund_amount: 102,
})
).rejects.toThrow(
`Claim has type "replace" but must be type "refund" to have a refund_amount.`
)
})
it("fails if type is unknown", async () => {
await expect(
claimService.create({
...testClaim,
type: "unknown",
})
).rejects.toThrow(`Claim type must be one of "refund" or "replace".`)
})
it("fails if no claim items", async () => {
await expect(
claimService.create({
...testClaim,
claim_items: [],
})
).rejects.toThrow(`Claims must have at least one claim item.`)
})
})
describe("retrieve", () => {
const claimRepo = MockRepository()
const claimService = new ClaimService({
manager: MockManager,
claimRepository: claimRepo,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully creates a claim", async () => {
claimRepo.setFindOne(() => Promise.resolve({ id: "claim_id" }))
await claimService.retrieve("claim_id", {
relations: ["order"],
})
expect(claimRepo.findOne).toHaveBeenCalledWith({
where: { id: "claim_id" },
relations: ["order"],
})
})
})
describe("createFulfillment", () => {
const fulfillmentService = {
createFulfillment: jest.fn((_, items) =>
Promise.resolve([{ id: "ful", items }])
),
withTransaction: function() {
withTransactionMock("fulfillment")
return this
},
}
const order = {
email: "hi@123",
discounts: [
{
id: "disc_1234",
},
],
payments: [{ id: "pay_test" }],
currency_code: "dkk",
tax_rate: 25,
region_id: "test_region",
display_id: 112345,
billing_address: { first_name: "hi" },
}
const claim = {
type: "replace",
fulfillment_status: "not_fulfilled",
order,
additional_items: [{ id: "item_test", quantity: 1 }],
shipping_methods: [{ id: "method_test" }],
}
const claimRepo = MockRepository({
findOne: () => Promise.resolve({ ...claim }),
})
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
withTransactionMock("lineItem")
return this
},
}
const claimService = new ClaimService({
manager: MockManager,
claimRepository: claimRepo,
fulfillmentService,
lineItemService,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully creates fulfillment", async () => {
await claimService.createFulfillment("claim_id", { meta: "data" })
expect(withTransactionMock).toHaveBeenCalledTimes(3)
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
expect(withTransactionMock).toHaveBeenCalledWith("fulfillment")
expect(fulfillmentService.createFulfillment).toHaveBeenCalledTimes(1)
expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith(
{
...claim,
payments: order.payments,
email: order.email,
discounts: order.discounts,
currency_code: order.currency_code,
tax_rate: order.tax_rate,
region_id: order.region_id,
display_id: order.display_id,
billing_address: order.billing_address,
items: claim.additional_items,
shipping_methods: claim.shipping_methods,
is_claim: true,
},
[
{
item_id: "item_test",
quantity: 1,
},
],
{ claim_order_id: "claim_id", metadata: { meta: "data" } }
)
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith("item_test", {
fulfilled_quantity: 1,
})
})
it("fails if no shipping_methods", async () => {
claimRepo.setFindOne(() =>
Promise.resolve({ ...claim, shipping_methods: [] })
)
await expect(
claimService.createFulfillment("claim_id", { meta: "data" })
).rejects.toThrow(`Cannot fulfill a claim without a shipping method.`)
})
it("fails if already fulfilled", async () => {
claimRepo.setFindOne(() =>
Promise.resolve({ ...claim, fulfillment_status: "fulfilled" })
)
await expect(
claimService.createFulfillment("claim_id", { meta: "data" })
).rejects.toThrow(`The claim has already been fulfilled.`)
})
it("fails if type is refund", async () => {
claimRepo.setFindOne(() => Promise.resolve({ ...claim, type: "refund" }))
await expect(
claimService.createFulfillment("claim_id", { meta: "data" })
).rejects.toThrow(`Claims with the type "refund" can not be fulfilled`)
})
})
describe("createShipment", () => {
const fulfillmentService = {
createShipment: jest.fn(() => {
return Promise.resolve({
items: [
{
item_id: "item_1",
quantity: 1,
},
],
})
}),
withTransaction: function() {
withTransactionMock("fulfillment")
return this
},
}
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
withTransactionMock("lineItem")
return this
},
}
const claimRepo = MockRepository()
const claimService = new ClaimService({
manager: MockManager,
claimRepository: claimRepo,
fulfillmentService,
lineItemService,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("calls fulfillment service", async () => {
claimRepo.setFindOne(() =>
Promise.resolve({
additional_items: [
{
id: "item_1",
shippied_quantity: 1,
},
],
})
)
await claimService.createShipment("claim", "ful_123", ["track1234"], {
meta: "data",
})
expect(withTransactionMock).toHaveBeenCalledTimes(3)
expect(withTransactionMock).toHaveBeenCalledWith("fulfillment")
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1)
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
"ful_123",
["track1234"],
{ meta: "data" }
)
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith("item_1", {
shipped_quantity: 1,
})
})
})
describe("cancel", () => {
const fulfillmentService = {
cancelFulfillment: jest.fn(),
withTransaction: function() {
withTransactionMock("fulfillment")
return this
},
}
const returnService = {
cancel: jest.fn(),
withTransaction: function() {
withTransactionMock("return")
return this
},
}
const claimRepo = MockRepository()
const claimService = new ClaimService({
manager: MockManager,
claimRepository: claimRepo,
fulfillmentService,
eventBusService,
returnService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("calls fulfillment service", async () => {
claimRepo.setFindOne(() =>
Promise.resolve({
return_order: {
id: "ret",
status: "requested",
},
fulfillments: [
{
id: "ful_21",
},
],
})
)
await claimService.cancel("claim")
expect(withTransactionMock).toHaveBeenCalledTimes(3)
expect(withTransactionMock).toHaveBeenCalledWith("fulfillment")
expect(withTransactionMock).toHaveBeenCalledWith("return")
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
expect(fulfillmentService.cancelFulfillment).toHaveBeenCalledTimes(1)
expect(fulfillmentService.cancelFulfillment).toHaveBeenCalledWith({
id: "ful_21",
})
expect(returnService.cancel).toHaveBeenCalledTimes(1)
expect(returnService.cancel).toHaveBeenCalledWith("ret")
})
it("fails if fulfillment is shipped", async () => {
claimRepo.setFindOne(() =>
Promise.resolve({
fulfillment_status: "shipped",
})
)
await expect(claimService.cancel("id")).rejects.toThrow(
"Cannot cancel a claim that has been shipped."
)
})
it("fails if return is received", async () => {
claimRepo.setFindOne(() =>
Promise.resolve({
return_order: {
id: "ret",
status: "received",
},
})
)
await expect(claimService.cancel("id")).rejects.toThrow(
"Cannot cancel a claim that has a received return."
)
})
})
})
+271
View File
@@ -0,0 +1,271 @@
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { Brackets } from "typeorm"
class ClaimItemService extends BaseService {
static Events = {
CREATED: "claim_item.created",
UPDATED: "claim_item.updated",
CANCELED: "claim_item.canceled",
}
constructor({
manager,
claimItemRepository,
claimTagRepository,
claimImageRepository,
lineItemService,
eventBusService,
}) {
super()
/** @private @constant {EntityManager} */
this.manager_ = manager
/** @private @constant {ClaimRepository} */
this.claimItemRepository_ = claimItemRepository
this.claimTagRepository_ = claimTagRepository
this.claimImageRepository_ = claimImageRepository
/** @private @constant {LineItemService} */
this.lineItemService_ = lineItemService
/** @private @constant {EventBus} */
this.eventBus_ = eventBusService
}
withTransaction(manager) {
if (!manager) {
return this
}
const cloned = new ClaimItemService({
manager,
claimItemRepository: this.claimItemRepository_,
claimTagRepository: this.claimTagRepository_,
claimImageRepository: this.claimImageRepository_,
lineItemService: this.lineItemService_,
eventBusService: this.eventBus_,
})
cloned.transactionManager_ = manager
return cloned
}
create(data) {
return this.atomicPhase_(async manager => {
const ciRepo = manager.getCustomRepository(this.claimItemRepository_)
const { item_id, reason, quantity, tags, images, ...rest } = data
if (
reason !== "missing_item" &&
reason !== "wrong_item" &&
reason !== "production_failure" &&
reason !== "other"
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claim Item reason must be one of "missing_item", "wrong_item", "production_failure" or "other".`
)
}
const lineItem = await this.lineItemService_
.withTransaction(manager)
.retrieve(item_id)
if (lineItem.fulfilled_quantity < quantity) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Cannot claim more of an item than has been fulfilled."
)
}
const claimTagRepo = manager.getCustomRepository(this.claimTagRepository_)
const tagsToAdd = await Promise.all(
tags.map(async t => {
const normalized = t.trim().toLowerCase()
const existing = await claimTagRepo.findOne({
where: { value: normalized },
})
if (existing) {
return existing
}
return claimTagRepo.create({ value: normalized })
})
)
const claimImgRepo = manager.getCustomRepository(
this.claimImageRepository_
)
const imagesToAdd = images.map(url => {
return claimImgRepo.create({ url })
})
const created = ciRepo.create({
...rest,
variant_id: lineItem.variant_id,
tags: tagsToAdd,
images: imagesToAdd,
item_id,
reason,
quantity,
})
const result = await ciRepo.save(created)
await this.eventBus_
.withTransaction(manager)
.emit(ClaimItemService.Events.CREATED, {
id: result.id,
})
return result
})
}
update(id, data) {
return this.atomicPhase_(async manager => {
const ciRepo = manager.getCustomRepository(this.claimItemRepository_)
const item = await this.retrieve(id, { relations: ["images", "tags"] })
const { tags, images, reason, note, metadata } = data
if (note) {
item.note = note
}
if (reason) {
item.reason = reason
}
if (metadata) {
item.metadata = this.setMetadata_(item, update.metadata)
}
if (tags) {
item.tags = []
const claimTagRepo = manager.getCustomRepository(
this.claimTagRepository_
)
for (const t of tags) {
if (t.id) {
item.tags.push(t)
} else {
const normalized = t.value.trim().toLowerCase()
const existing = await claimTagRepo.findOne({
where: { value: normalized },
})
if (existing) {
item.tags.push(existing)
} else {
item.tags.push(claimTagRepo.create({ value: normalized }))
}
}
}
}
if (images) {
const claimImgRepo = manager.getCustomRepository(
this.claimImageRepository_
)
const ids = images.map(i => i.id)
for (const i of item.images) {
if (!ids.includes(i.id)) {
await claimImgRepo.remove(i)
}
}
item.images = []
for (const i of images) {
if (i.id) {
item.images.push(i)
} else {
item.images.push(claimImgRepo.create({ url: i.url }))
}
}
}
await ciRepo.save(item)
await this.eventBus_
.withTransaction(manager)
.emit(ClaimItemService.Events.UPDATED, {
id: item.id,
})
return item
})
}
async cancel(id) {}
/**
* @param {Object} selector - the query object for find
* @return {Promise} the result of the find operation
*/
async list(
selector,
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const ciRepo = this.manager_.getCustomRepository(this.claimItemRepository_)
const query = this.buildQuery_(selector, config)
return ciRepo.find(query)
}
/**
* Gets an order by id.
* @param {string} orderId - id of order to retrieve
* @return {Promise<Order>} the order document
*/
async retrieve(id, config = {}) {
const claimItemRepo = this.manager_.getCustomRepository(
this.claimItemRepository_
)
const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config)
const item = await claimItemRepo.findOne(query)
if (!item) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Claim item with id: ${id} was not found.`
)
}
return item
}
/**
* Dedicated method to delete metadata for an order.
* @param {string} orderId - the order to delete metadata from.
* @param {string} key - key for metadata field
* @return {Promise} resolves to the updated result.
*/
async deleteMetadata(orderId, key) {
const validatedId = this.validateId_(orderId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.orderModel_
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
}
export default ClaimItemService
+575
View File
@@ -0,0 +1,575 @@
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { Brackets } from "typeorm"
class ClaimService extends BaseService {
static Events = {
CREATED: "claim.created",
UPDATED: "claim.updated",
CANCELED: "claim.canceled",
FULFILLMENT_CREATED: "claim.fulfillment_created",
SHIPMENT_CREATED: "claim.shipment_created",
REFUND_PROCESSED: "claim.refund_processed",
}
constructor({
manager,
claimRepository,
fulfillmentProviderService,
fulfillmentService,
lineItemService,
totalsService,
paymentProviderService,
returnService,
shippingOptionService,
claimItemService,
regionService,
eventBusService,
}) {
super()
/** @private @constant {EntityManager} */
this.manager_ = manager
/** @private @constant {ClaimRepository} */
this.claimRepository_ = claimRepository
/** @private @constant {FulfillmentProviderService} */
this.fulfillmentProviderService_ = fulfillmentProviderService
/** @private @constant {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @constant {LineItemService} */
this.lineItemService_ = lineItemService
/** @private @constant {RegionService} */
this.regionService_ = regionService
/** @private @constant {ReturnService} */
this.returnService_ = returnService
/** @private @constant {FulfillmentService} */
this.fulfillmentService_ = fulfillmentService
/** @private @constant {ClaimItemService} */
this.claimItemService_ = claimItemService
/** @private @constant {TotalsService} */
this.totalsService_ = totalsService
/** @private @constant {EventBus} */
this.eventBus_ = eventBusService
/** @private @constant {ShippingOptionService} */
this.shippingOptionService_ = shippingOptionService
}
withTransaction(manager) {
if (!manager) {
return this
}
const cloned = new ClaimService({
manager,
claimRepository: this.claimRepository_,
fulfillmentProviderService: this.fulfillmentProviderService_,
fulfillmentService: this.fulfillmentService_,
paymentProviderService: this.paymentProviderService_,
lineItemService: this.lineItemService_,
regionService: this.regionService_,
returnService: this.returnService_,
claimItemService: this.claimItemService_,
eventBusService: this.eventBus_,
totalsService: this.totalsService_,
shippingOptionService: this.shippingOptionService_,
})
cloned.transactionManager_ = manager
return cloned
}
update(id, data) {
return this.atomicPhase_(async manager => {
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const claim = await this.retrieve(id, { relations: ["shipping_methods"] })
const { claim_items, shipping_methods, metadata } = data
if (metadata) {
claim.metadata = this.setMetadata_(claim, update.metadata)
await claimRepo.save(claim)
}
if (shipping_methods) {
for (const m of claim.shipping_methods) {
await this.shippingOptionService_
.withTransaction(manager)
.updateShippingMethod(m.id, {
claim_order_id: null,
})
}
for (const method of shipping_methods) {
if (method.id) {
await this.shippingOptionService_
.withTransaction(manager)
.updateShippingMethod(method.id, {
claim_order_id: claim.id,
})
} else {
await this.shippingOptionService_
.withTransaction(manager)
.createShippingMethod(method.option_id, method.data, {
claim_order_id: claim.id,
price: method.price,
})
}
}
}
if (claim_items) {
for (const i of claim_items) {
if (i.id) {
await this.claimItemService_
.withTransaction(manager)
.update(i.id, i)
}
}
}
await this.eventBus_
.withTransaction(manager)
.emit(ClaimService.Events.UPDATED, {
id: claim.id,
})
return claim
})
}
create(data) {
return this.atomicPhase_(async manager => {
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const {
type,
claim_items,
order,
return_shipping,
additional_items,
shipping_methods,
refund_amount,
...rest
} = data
if (type !== "refund" && type !== "replace") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claim type must be one of "refund" or "replace".`
)
}
if (type === "replace" && !additional_items?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claims with type "replace" must have at least one additional item.`
)
}
if (!claim_items?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claims must have at least one claim item.`
)
}
if (refund_amount && type !== "refund") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Claim has type "${type}" but must be type "refund" to have a refund_amount.`
)
}
let toRefund = refund_amount
if (type === "refund" && typeof refund_amount === "undefined") {
const lines = claim_items.map(ci => {
const orderItem = order.items.find(oi => oi.id === ci.item_id)
return {
...orderItem,
quantity: ci.quantity,
}
})
toRefund = await this.totalsService_.getRefundTotal(order, lines)
}
const newItems = await Promise.all(
additional_items.map(i =>
this.lineItemService_
.withTransaction(manager)
.generate(i.variant_id, order.region_id, i.quantity)
)
)
const created = claimRepo.create({
shipping_address_id: order.shipping_address_id,
payment_status: type === "refund" ? "not_refunded" : "na",
...rest,
refund_amount: toRefund,
type,
additional_items: newItems,
order_id: order.id,
})
const result = await claimRepo.save(created)
if (shipping_methods) {
for (const method of shipping_methods) {
if (method.id) {
await this.shippingOptionService_
.withTransaction(manager)
.updateShippingMethod(method.id, {
claim_order_id: result.id,
})
} else {
await this.shippingOptionService_
.withTransaction(manager)
.createShippingMethod(method.option_id, method.data, {
claim_order_id: result.id,
price: method.price,
})
}
}
}
for (const ci of claim_items) {
await this.claimItemService_.withTransaction(manager).create({
...ci,
claim_order_id: result.id,
})
}
if (return_shipping) {
await this.returnService_.withTransaction(manager).create(
{
claim_order_id: result.id,
items: claim_items.map(ci => ({
item_id: ci.item_id,
quantity: ci.quantity,
metadata: ci.metadata,
})),
shipping_method: return_shipping,
},
order
)
}
await this.eventBus_
.withTransaction(manager)
.emit(ClaimService.Events.CREATED, {
id: result.id,
})
return result
})
}
createFulfillment(id, metadata = {}) {
return this.atomicPhase_(async manager => {
const claim = await this.retrieve(id, {
relations: [
"additional_items",
"shipping_methods",
"shipping_address",
"order",
"order.billing_address",
"order.discounts",
"order.payments",
],
})
const order = claim.order
if (claim.fulfillment_status !== "not_fulfilled") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The claim has already been fulfilled."
)
}
if (claim.type !== "replace") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Claims with the type "${claim.type}" can not be fulfilled.`
)
}
if (!claim.shipping_methods?.length) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Cannot fulfill a claim without a shipping method."
)
}
const fulfillments = await this.fulfillmentService_
.withTransaction(manager)
.createFulfillment(
{
...claim,
email: order.email,
payments: order.payments,
discounts: order.discounts,
currency_code: order.currency_code,
tax_rate: order.tax_rate,
region_id: order.region_id,
display_id: order.display_id,
billing_address: order.billing_address,
items: claim.additional_items,
shipping_methods: claim.shipping_methods,
is_claim: true,
},
claim.additional_items.map(i => ({
item_id: i.id,
quantity: i.quantity,
})),
{ claim_order_id: id, metadata }
)
let successfullyFulfilled = []
for (const f of fulfillments) {
successfullyFulfilled = successfullyFulfilled.concat(f.items)
}
claim.fulfillment_status = "fulfilled"
for (const item of claim.additional_items) {
const fulfillmentItem = successfullyFulfilled.find(
f => item.id === f.item_id
)
if (fulfillmentItem) {
const fulfilledQuantity =
(item.fulfilled_quantity || 0) + fulfillmentItem.quantity
// Update the fulfilled quantity
await this.lineItemService_.withTransaction(manager).update(item.id, {
fulfilled_quantity: fulfilledQuantity,
})
if (item.quantity !== fulfilledQuantity) {
claim.fulfillment_status = "requires_action"
}
} else {
if (item.quantity !== item.fulfilled_quantity) {
claim.fulfillment_status = "requires_action"
}
}
}
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const result = await claimRepo.save(claim)
for (const fulfillment of fulfillments) {
await this.eventBus_
.withTransaction(manager)
.emit(ClaimService.Events.FULFILLMENT_CREATED, {
id: id,
fulfillment_id: fulfillment.id,
})
}
return result
})
}
async processRefund(id) {
return this.atomicPhase_(async manager => {
const claim = await this.retrieve(id, {
relations: ["order", "order.payments"],
})
if (claim.type !== "refund") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Claim must have type "refund" to create a refund.`
)
}
if (claim.refund_amount) {
await this.paymentProviderService_
.withTransaction(manager)
.refundPayment(claim.order.payments, claim.refund_amount, "claim")
}
claim.payment_status = "refunded"
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const result = await claimRepo.save(claim)
await this.eventBus_
.withTransaction(manager)
.emit(ClaimService.Events.REFUND_PROCESSED, {
id,
})
return result
})
}
async createShipment(id, fulfillmentId, trackingNumbers, metadata = []) {
return this.atomicPhase_(async manager => {
const claim = await this.retrieve(id, {
relations: ["additional_items"],
})
const shipment = await this.fulfillmentService_
.withTransaction(manager)
.createShipment(fulfillmentId, trackingNumbers, metadata)
claim.fulfillment_status = "shipped"
for (const i of claim.additional_items) {
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, {
shipped_quantity: shippedQty,
})
if (shippedQty !== i.quantity) {
claim.fulfillment_status = "partially_shipped"
}
} else {
if (i.shipped_quantity !== i.quantity) {
claim.fulfillment_status = "partially_shipped"
}
}
}
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const result = await claimRepo.save(claim)
await this.eventBus_
.withTransaction(manager)
.emit(ClaimService.Events.SHIPMENT_CREATED, {
id,
fulfillment_id: shipment.id,
})
return result
})
}
async cancel(id) {
return this.atomicPhase_(async manager => {
const claim = await this.retrieve(id, {
relations: ["return_order", "fulfillments"],
})
if (
claim.fulfillment_status === "shipped" ||
claim.fulfillment_status === "partially_shipped"
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot cancel a claim that has been shipped.`
)
}
if (claim?.return_order?.status === "received") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot cancel a claim that has a received return.`
)
}
if (claim.return_order) {
await this.returnService_
.withTransaction(manager)
.cancel(claim.return_order.id)
}
await Promise.all(
claim.fulfillments.map(f =>
this.fulfillmentService_.withTransaction(manager).cancelFulfillment(f)
)
)
claim.fulfillment_status = "canceled"
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const result = await claimRepo.save(claim)
await this.eventBus_
.withTransaction(manager)
.emit(ClaimService.Events.CANCELED, {
id: result.id,
})
return result
})
}
/**
* @param {Object} selector - the query object for find
* @return {Promise} the result of the find operation
*/
async list(
selector,
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const claimRepo = this.manager_.getCustomRepository(this.claimRepository_)
const query = this.buildQuery_(selector, config)
return claimRepo.find(query)
}
/**
* Gets an order by id.
* @param {string} orderId - id of order to retrieve
* @return {Promise<Order>} the order document
*/
async retrieve(claimId, config = {}) {
const claimRepo = this.manager_.getCustomRepository(this.claimRepository_)
const validatedId = this.validateId_(claimId)
const query = this.buildQuery_({ id: validatedId }, config)
const claim = await claimRepo.findOne(query)
if (!claim) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Claim with ${claimId} was not found`
)
}
return claim
}
/**
* Dedicated method to delete metadata for an order.
* @param {string} orderId - the order to delete metadata from.
* @param {string} key - key for metadata field
* @return {Promise} resolves to the updated result.
*/
async deleteMetadata(orderId, key) {
const validatedId = this.validateId_(orderId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.orderModel_
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
}
export default ClaimService
+1
View File
@@ -311,6 +311,7 @@ class ReturnService extends BaseService {
"shipping_method",
"shipping_method.shipping_option",
"swap",
"claim_order",
],
})
@@ -177,6 +177,10 @@ class ShippingOptionService extends BaseService {
method.order_id = update.order_id
}
if ("claim_order_id" in update) {
method.claim_order_id = update.claim_order_id
}
return methodRepo.save(method)
})
}
@@ -246,6 +250,10 @@ class ShippingOptionService extends BaseService {
toCreate.order_id = config.order_id
}
if (config.claim_order_id) {
toCreate.claim_order_id = config.claim_order_id
}
const method = await methodRepo.create(toCreate)
const created = await methodRepo.save(method)
+4 -4
View File
@@ -5589,10 +5589,10 @@ medusa-core-utils@^1.1.0:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-test-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.0.tgz#283189ba1bf1710bced86b6e4a1e25b71e0a0f4c"
integrity sha512-eLAMd2n72xI1z3KecHuVyyr96acyylNR3SDP3DuC+4O3TOAg18EzFu3/W53z8MI2I5bBgGhQc7sEsN+krfVBJg==
medusa-test-utils@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.1.tgz#e6c9faf9339a2fa22f162c9864091d6e12792215"
integrity sha512-Fx55x1widi9yz3tQSvQbkACyNZMu05j9q9Ta0eDWwb1q96RpTQg0BMc0Em+ZXE9EwxWHqgDUqlAfLGmCW6aQUg==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "^1.1.0"
+1011 -33
View File
File diff suppressed because it is too large Load Diff