merge develop

This commit is contained in:
Sebastian Rindom
2021-10-15 20:09:31 +02:00
parent 10c87e8d5a
commit 4fd361fddd
207 changed files with 8699 additions and 4124 deletions

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -96,4 +96,33 @@ Joi.orderFilter = () => {
})
}
Joi.productFilter = () => {
return Joi.object().keys({
id: Joi.string(),
q: Joi.string().allow(null, ""),
status: Joi.array()
.items(Joi.string().valid("proposed", "draft", "published", "rejected"))
.single(),
collection_id: Joi.array()
.items(Joi.string())
.single(),
tags: Joi.array()
.items(Joi.string())
.single(),
title: Joi.string(),
description: Joi.string(),
handle: Joi.string(),
is_giftcard: Joi.string(),
type: Joi.string(),
offset: Joi.string(),
limit: Joi.string(),
expand: Joi.string(),
fields: Joi.string(),
order: Joi.string().optional(),
created_at: Joi.dateFilter(),
updated_at: Joi.dateFilter(),
deleted_at: Joi.dateFilter(),
})
}
export default Joi

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -0,0 +1,15 @@
# medusa-file-s3
Upload files to an AWS S3 bucket.
## Options
```
s3_url: [url of your s3 bucket],
access_key_id: [access-key],
secret_access_key: [secret-access-key],
bucket: [name of your bucket],
region: [region of your bucket],
```
Follow [this guide](https://docs.medusa-commerce.com/how-to/uploading-images-to-s3) to configure the plugin.

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -0,0 +1,15 @@
# medusa-file-spaces
Upload files to a DigitalOcean Space.
## Options
```
spaces_url: [url of your DigitalOcean space],
access_key_id: [access-key],
secret_access_key: [secret-access-key],
bucket: [name of your bucket],
endpoint: [endpoint of you DigitalOcean space],
```
Follow [this guide](https://docs.medusa-commerce.com/how-to/uploading-images-to-spaces) to configure the plugin.

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

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

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -106,7 +106,13 @@ class WebshipperFulfillmentService extends FulfillmentService {
const fromOrder = await this.orderService_.retrieve(orderId, {
select: ["total"],
relations: ["discounts", "shipping_address", "returns"],
relations: [
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_address",
"returns",
],
})
const methodData = returnOrder.shipping_method.data
@@ -286,10 +292,11 @@ class WebshipperFulfillmentService extends FulfillmentService {
)) &&
this.invoiceGenerator_.createCertificateOfOrigin
) {
const base64Coo = await this.invoiceGenerator_.createCertificateOfOrigin(
fromOrder,
fulfillmentItems
)
const base64Coo =
await this.invoiceGenerator_.createCertificateOfOrigin(
fromOrder,
fulfillmentItems
)
certificateOfOrigin = await this.client_.documents
.create({
@@ -419,9 +426,8 @@ class WebshipperFulfillmentService extends FulfillmentService {
url: l.url,
tracking_number: l.number,
}))
const [orderId, fulfillmentIndex] = wsOrder.data.attributes.ext_ref.split(
"."
)
const [orderId, fulfillmentIndex] =
wsOrder.data.attributes.ext_ref.split(".")
if (orderId.charAt(0).toLowerCase() === "s") {
if (fulfillmentIndex.startsWith("ful")) {

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -0,0 +1,241 @@
import _ from "lodash"
import Stripe from "stripe"
import { PaymentService } from "medusa-interfaces"
class BancontactProviderService extends PaymentService {
static identifier = "stripe-bancontact"
constructor(
{ stripeProviderService, customerService, totalsService, regionService },
options
) {
super()
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {object} paymentData - payment method data from cart
* @returns {string} the status of the payment intent
*/
async getStatus(paymentData) {
return await this.stripeProviderService_.getStatus(paymentData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @returns {Promise<Array<object>>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @returns {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @returns {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {object} cart - cart to create a payment for
* @returns {object} Stripe payment intent
*/
async createPayment(cart) {
const { customer_id, region_id, email } = cart
const region = await this.regionService_.retrieve(region_id)
const { currency_code } = region
const amount = await this.totalsService_.getTotal(cart)
const intentRequest = {
amount: Math.round(amount),
currency: currency_code,
payment_method_types: ["bancontact"],
capture_method: "automatic",
metadata: { cart_id: `${cart.id}` },
}
if (customer_id) {
const customer = await this.customerService_.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
const paymentIntent = await this.stripe_.paymentIntents.create(
intentRequest
)
return paymentIntent
}
/**
* Retrieves Stripe payment intent.
* @param {object} data - the data of the payment to retrieve
* @returns {Promise<object>} Stripe payment intent
*/
async retrievePayment(data) {
return await this.stripeProviderService_.retrievePayment(data)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {object} sessionData - the data of the payment to retrieve
* @returns {Promise<object>} Stripe payment intent
*/
async getPaymentData(sessionData) {
return await this.stripeProviderService_.getPaymentData(sessionData)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {object} sessionData - payment session data
* @param {object} context - properties relevant to current context
* @returns {Promise<{ status: string, data: object }>} result with data and status
*/
async authorizePayment(sessionData, context = {}) {
return await this.stripeProviderService_.authorizePayment(
sessionData,
context
)
}
async updatePaymentData(sessionData, update) {
return await this.stripeProviderService_.updatePaymentData(
sessionData,
update
)
}
/**
* Updates Stripe payment intent.
* @param {object} sessionData - payment session data.
* @param {object} update - objec to update intent with
* @returns {object} Stripe payment intent
*/
async updatePayment(sessionData, cart) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== sessionData.customer) {
return this.createPayment(cart)
} else {
if (cart.total && sessionData.amount === Math.round(cart.total)) {
return sessionData
}
return this.stripe_.paymentIntents.update(sessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async deletePayment(payment) {
return await this.stripeProviderService_.deletePayment(payment)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @returns {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {object} paymentData - payment method data from cart
* @returns {object} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {object} paymentData - payment method data from cart
* @param {number} amountToRefund - amount to refund
* @returns {string} refunded payment intent
*/
async refundPayment(payment, amountToRefund) {
return await this.stripeProviderService_.refundPayment(
payment,
amountToRefund
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {object} paymentData - payment method data from cart
* @returns {object} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
export default BancontactProviderService

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ContentfulMock = void 0;
var ContentfulMock = {
createClient: jest.fn()
};
exports.ContentfulMock = ContentfulMock;

View File

@@ -0,0 +1,6 @@
export const createClient = jest.fn()
const mock = jest.fn().mockImplementation(() => {
return { createClient }
})
export default mock

View File

@@ -0,0 +1,179 @@
import ContentfulService from "../contentful"
describe("ContentfulService", () => {
describe("delete in medusa", () => {
const regionService = {
retrieve: jest.fn((id) => {
if (id === "exists") {
return Promise.resolve({ id: "exists" })
}
return Promise.resolve(undefined)
}),
}
const productService = {
retrieve: jest.fn((id) => {
if (id === "exists") {
return Promise.resolve({ id: "exists" })
}
return Promise.resolve(undefined)
}),
}
const redisClient = {
get: async (id) => {
// const key = `${id}_ignore_${side}`
if (id === `ignored_ignore_contentful`) {
return { id }
}
return undefined
},
set: async (id) => {
return undefined
},
}
const productVariantService = {
retrieve: jest.fn((id) => {
if (id === "exists") {
return Promise.resolve({ id: "exists" })
}
return Promise.resolve(undefined)
}),
}
const eventBusService = {}
const service = new ContentfulService(
{
regionService,
productService,
redisClient,
productVariantService,
eventBusService,
},
{
space_id: "test_id",
environment: "master",
access_token: "test_token",
}
)
const entry = {
unpublish: jest.fn(async () => {
return {
id: "id",
}
}),
archive: jest.fn(async () => {
return {
id: "id",
}
}),
}
service.contentful_ = {
getSpace: async (space_id) => {
return {
getEnvironment: async (env) => {
return {
getEntry: async (id) => {
if (id === "onlyMedusa") {
throw new Error("doesn't exist")
}
return entry
},
}
},
}
},
}
beforeEach(() => {
jest.clearAllMocks()
})
describe("archiveProductInContentful", () => {
it("Calls entry.unpublish and entry.archive", async () => {
await service.archiveProductInContentful({ id: "test" })
expect(entry.unpublish).toHaveBeenCalledTimes(1)
expect(entry.archive).toHaveBeenCalledTimes(1)
})
it("Doesn't call entry.unpublish and entry.archive if the product still exists in medusa", async () => {
await service.archiveProductInContentful({ id: "exists" })
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
it("Doesn't call productService if request should be ignored", async () => {
await service.archiveProductInContentful({ id: "ignored" })
expect(productService.retrieve).toHaveBeenCalledTimes(0)
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
})
describe("archiveProductVariantInContentful", () => {
it("Calls entry.unpublish and entry.archive", async () => {
await service.archiveProductVariantInContentful({ id: "test" })
expect(entry.unpublish).toHaveBeenCalledTimes(1)
expect(entry.archive).toHaveBeenCalledTimes(1)
})
it("Doesn't call entry.unpublish and entry.archive if the variant still exists in medusa", async () => {
await service.archiveProductVariantInContentful({ id: "exists" })
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
it("Doesn't call productVariantService if request should be ignored", async () => {
await service.archiveProductVariantInContentful({ id: "ignored" })
expect(productVariantService.retrieve).toHaveBeenCalledTimes(0)
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
})
describe("archiveRegionInContentful", () => {
it("Calls entry.unpublish and entry.archive", async () => {
await service.archiveRegionInContentful({ id: "test" })
expect(entry.unpublish).toHaveBeenCalledTimes(1)
expect(entry.archive).toHaveBeenCalledTimes(1)
})
it("Doesn't call entry.unpublish and entry.archive if the region still exists in medusa", async () => {
await service.archiveRegionInContentful({ id: "exists" })
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
it("Doesn't call RegionService if request should be ignored", async () => {
await service.archiveRegionInContentful({ id: "ignored" })
expect(regionService.retrieve).toHaveBeenCalledTimes(0)
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
})
describe("archiveEntryWidthId", () => {
it("Calls archive if entry exists", async () => {
await service.archiveEntryWidthId("exists")
expect(entry.unpublish).toHaveBeenCalledTimes(1)
expect(entry.archive).toHaveBeenCalledTimes(1)
})
it("Doesnt call archive if entry doesn't exists", async () => {
await service.archiveEntryWidthId("onlyMedusa")
expect(entry.unpublish).toHaveBeenCalledTimes(0)
expect(entry.archive).toHaveBeenCalledTimes(0)
})
})
})
})

View File

@@ -91,7 +91,7 @@ class ContentfulService extends BaseService {
async createImageAssets(product) {
const environment = await this.getContentfulEnvironment_()
let assets = []
const assets = []
await Promise.all(
product.images
.filter((image) => image.url !== product.thumbnail)
@@ -646,6 +646,92 @@ class ContentfulService extends BaseService {
}
}
async archiveProductVariantInContentful(variant) {
let variantEntity
try {
const ignore = await this.shouldIgnore_(variant.id, "contentful")
if (ignore) {
return Promise.resolve()
}
try {
variantEntity = await this.productVariantService_.retrieve(variant.id)
} catch (err) {
// ignore
}
if (variantEntity) {
return Promise.resolve()
}
return await this.archiveEntryWidthId(variant.id)
} catch (error) {
throw error
}
}
async archiveProductInContentful(product) {
let productEntity
try {
const ignore = await this.shouldIgnore_(product.id, "contentful")
if (ignore) {
return Promise.resolve()
}
try {
productEntity = await this.productService_.retrieve(product.id)
} catch (err) {}
if (productEntity) {
return Promise.resolve()
}
return await this.archiveEntryWidthId(product.id)
} catch (error) {
throw error
}
}
async archiveRegionInContentful(region) {
let regionEntity
try {
const ignore = await this.shouldIgnore_(region.id, "contentful")
if (ignore) {
return Promise.resolve()
}
try {
regionEntity = await this.regionService_.retrieve(region.id)
} catch (err) {}
if (regionEntity) {
return Promise.resolve()
}
return await this.archiveEntryWidthId(region.id)
} catch (error) {
throw error
}
}
async archiveEntryWidthId(id) {
const environment = await this.getContentfulEnvironment_()
// check if product exists
let entry = undefined
try {
entry = await environment.getEntry(id)
} catch (error) {
return Promise.resolve()
}
const unpublishEntry = await entry.unpublish()
const archivedEntry = await entry.archive()
await this.addIgnore_(id, "medusa")
return archivedEntry
}
async sendContentfulProductToAdmin(productId) {
const ignore = await this.shouldIgnore_(productId, "medusa")
if (ignore) {
@@ -658,7 +744,7 @@ class ContentfulService extends BaseService {
const product = await this.productService_.retrieve(productId)
let update = {}
const update = {}
const title =
productEntry.fields[this.getCustomField("title", "product")]["en-US"]
@@ -741,9 +827,9 @@ class ContentfulService extends BaseService {
isArray = false
}
let output = []
const output = []
for (const obj of input) {
let transformed = Object.assign({}, obj)
const transformed = Object.assign({}, obj)
transformed.medusaId = obj.id
output.push(transformed)
}

View File

@@ -18,10 +18,18 @@ class ContentfulSubscriber {
await this.contentfulService_.updateRegionInContentful(data)
})
this.eventBus_.subscribe("region.deleted", async (data) => {
await this.contentfulService_.updateRegionInContentful(data)
})
this.eventBus_.subscribe("product-variant.updated", async (data) => {
await this.contentfulService_.updateProductVariantInContentful(data)
})
this.eventBus_.subscribe("product-variant.deleted", async (data) => {
await this.contentfulService_.archiveProductVariantInContentful(data)
})
this.eventBus_.subscribe("product.updated", async (data) => {
await this.contentfulService_.updateProductInContentful(data)
})
@@ -29,6 +37,10 @@ class ContentfulSubscriber {
this.eventBus_.subscribe("product.created", async (data) => {
await this.contentfulService_.createProductInContentful(data)
})
this.eventBus_.subscribe("product.deleted", async (data) => {
await this.contentfulService_.archiveProductInContentful(data)
})
}
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -59,7 +59,9 @@ class MeiliSearchService extends SearchService {
}
transformProducts(products) {
if (!products) return []
if (!products) {
return []
}
return products.map(transformProduct)
}
}

View File

@@ -20,9 +20,8 @@ export const transformProduct = (product) => {
variantKeys.forEach((k) => {
if (k === "options" && variant[k]) {
const values = variant[k].map((option) => option.value)
obj[`${prefix}_options_value`] = obj[`${prefix}_options_value`].concat(
values
)
obj[`${prefix}_options_value`] =
obj[`${prefix}_options_value`].concat(values)
return
}
return variant[k] && obj[`${prefix}_${k}`].push(variant[k])

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -135,6 +135,57 @@ describe("RestockNotificationService", () => {
})
describe("triggerRestock", () => {
afterEach(() => {
jest.useRealTimers()
})
it("trigger delay default to 0", async () => {
const restockNotiService = new RestockNotificationService({
manager: MockManager,
productVariantService: ProductVariantService,
restockNotificationModel: RestockNotificationModel,
eventBusService: EventBusService,
})
restockNotiService.restockExecute_ = jest.fn()
jest.clearAllMocks()
jest.useFakeTimers()
restockNotiService.triggerRestock("variant_test")
jest.runAllTimers()
expect(setTimeout).toHaveBeenCalledTimes(1)
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 0)
})
it("trigger delay 10", async () => {
const restockNotiService = new RestockNotificationService(
{
manager: MockManager,
productVariantService: ProductVariantService,
restockNotificationModel: RestockNotificationModel,
eventBusService: EventBusService,
},
{ trigger_delay: 10 }
)
restockNotiService.restockExecute_ = jest.fn()
jest.clearAllMocks()
jest.useFakeTimers()
restockNotiService.triggerRestock("variant_test")
jest.runAllTimers()
expect(setTimeout).toHaveBeenCalledTimes(1)
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 10)
})
})
describe("restockExecute_", () => {
const restockNotiService = new RestockNotificationService({
manager: MockManager,
productVariantService: ProductVariantService,
@@ -145,20 +196,20 @@ describe("RestockNotificationService", () => {
it("non-existing noti does nothing", async () => {
jest.clearAllMocks()
await expect(restockNotiService.triggerRestock("variant_test")).resolves
await expect(restockNotiService.restockExecute_("variant_test")).resolves
})
it("existing noti but out of stock does nothing", async () => {
jest.clearAllMocks()
await expect(restockNotiService.triggerRestock("variant_outofstock"))
await expect(restockNotiService.restockExecute_("variant_outofstock"))
.resolves
})
it("existing noti emits and deletes", async () => {
jest.clearAllMocks()
await restockNotiService.triggerRestock("variant_1234")
await restockNotiService.restockExecute_("variant_1234")
expect(EventBusService.emit).toHaveBeenCalledTimes(1)
expect(EventBusService.emit).toHaveBeenCalledWith(
@@ -187,7 +238,7 @@ describe("RestockNotificationService", () => {
{ inventory_required: 5 }
)
await service.triggerRestock("variant_1234")
await service.restockExecute_("variant_1234")
expect(EventBusService.emit).toHaveBeenCalledTimes(1)
expect(EventBusService.emit).toHaveBeenCalledWith(
@@ -214,7 +265,7 @@ describe("RestockNotificationService", () => {
{ inventory_required: 5 }
)
await service.triggerRestock("variant_low_inventory")
await service.restockExecute_("variant_low_inventory")
expect(EventBusService.emit).toHaveBeenCalledTimes(0)
expect(RestockNotificationModel.delete).toHaveBeenCalledTimes(0)

View File

@@ -108,10 +108,15 @@ class RestockNotificationService extends BaseService {
* and emits a restocked event to the event bus. After successful emission the
* restock notification is deleted.
* @param {string} variantId - the variant id to trigger restock for
* @return {Promise<RestockNotification>} The resulting restock notification
* @return The resulting restock notification
*/
async triggerRestock(variantId) {
return this.atomicPhase_(async (manager) => {
triggerRestock(variantId) {
const delay = this.options_?.trigger_delay ?? 0
setTimeout(() => this.restockExecute_(variantId), delay)
}
async restockExecute_(variantId) {
return await this.atomicPhase_(async (manager) => {
const restockRepo = manager.getRepository(this.restockNotificationModel_)
const existing = await this.retrieve(variantId)

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -44,6 +44,8 @@ class OrderSubscriber {
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",
@@ -146,6 +148,8 @@ class OrderSubscriber {
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",
@@ -253,6 +257,8 @@ class OrderSubscriber {
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -320,6 +320,8 @@ class SendGridService extends NotificationService {
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"shipping_methods.shipping_option",
"payments",
@@ -363,6 +365,8 @@ class SendGridService extends NotificationService {
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"shipping_methods.shipping_option",
"payments",
@@ -496,6 +500,8 @@ class SendGridService extends NotificationService {
relations: [
"items",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_address",
"returns",
"swaps",
@@ -601,6 +607,8 @@ class SendGridService extends NotificationService {
relations: [
"items",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_address",
"swaps",
"swaps.additional_items",
@@ -686,7 +694,14 @@ class SendGridService extends NotificationService {
})
const order = await this.orderService_.retrieve(swap.order_id, {
relations: ["items", "discounts", "swaps", "swaps.additional_items"],
relations: [
"items",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"swaps",
"swaps.additional_items",
],
})
let merged = [...order.items]

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -40,6 +40,8 @@ class SlackService extends BaseService {
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,9 +0,0 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -63,6 +63,7 @@
"glob": "^7.1.6",
"ioredis": "^4.17.3",
"ioredis-mock": "^5.6.0",
"iso8601-duration": "^1.3.0",
"joi": "^17.3.0",
"joi-objectid": "^3.0.1",
"jsonwebtoken": "^8.5.1",
@@ -87,4 +88,4 @@
"winston": "^3.2.1"
},
"gitHead": "41a5425405aea5045a26def95c0dc00cf4a5a44d"
}
}

View File

@@ -0,0 +1,17 @@
import _ from "lodash"
/**
* @oas [get] /auth
* operationId: "DeleteAuth"
* summary: "Delete Session"
* description: "Deletes the current session for the logged in user."
* tags:
* - Auth
* responses:
* "200":
* description: OK
*/
export default async (req, res) => {
req.session.destroy()
res.status(200).end()
}

View File

@@ -13,5 +13,11 @@ export default app => {
)
route.post("/", middlewares.wrap(require("./create-session").default))
route.delete(
"/",
middlewares.authenticate(),
middlewares.wrap(require("./delete-session").default)
)
return app
}

View File

@@ -46,6 +46,7 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
],
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
}

View File

@@ -46,6 +46,7 @@ describe("POST /admin/discounts/:discount_id/variants/:variant_id", () => {
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
],
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
}

View File

@@ -16,6 +16,8 @@ describe("POST /admin/discounts", () => {
value: 10,
allocation: "total",
},
starts_at: "02/02/2021 13:45",
ends_at: "03/14/2021 04:30",
},
adminSession: {
jwt: {
@@ -39,12 +41,99 @@ describe("POST /admin/discounts", () => {
value: 10,
allocation: "total",
},
starts_at: new Date("02/02/2021 13:45"),
ends_at: new Date("03/14/2021 04:30"),
is_disabled: false,
is_dynamic: false,
})
})
})
describe("unsuccessful creation with dynamic discount using an invalid iso8601 duration", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "PaMT2D",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message[0].message).toEqual(
`"valid_duration" must be a valid ISO 8601 duration`
)
})
})
describe("successful creation with dynamic discount", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service create", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: new Date("02/02/2021 13:45"),
is_disabled: false,
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
})
})
})
describe("fails on invalid data", () => {
let subject
@@ -74,4 +163,84 @@ describe("POST /admin/discounts", () => {
expect(subject.body.message[0].message).toEqual(`"rule.type" is required`)
})
})
describe("fails on invalid date intervals", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
ends_at: "02/02/2021",
starts_at: "03/14/2021",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message[0].message).toEqual(
`"ends_at" must be greater than "ref:starts_at"`
)
})
})
describe("succesfully creates a dynamic discount without setting valid duration", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
is_dynamic: true,
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: "03/14/2021 14:30",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns error", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
code: "TEST",
is_dynamic: true,
is_disabled: false,
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: new Date("03/14/2021 14:30"),
})
})
})
})

View File

@@ -17,6 +17,7 @@ const defaultFields = [
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
const defaultRelations = [

View File

@@ -17,6 +17,7 @@ const defaultFields = [
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
const defaultRelations = [

View File

@@ -17,6 +17,7 @@ const defaultFields = [
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
const defaultRelations = [

View File

@@ -7,6 +7,7 @@ describe("POST /admin/discounts", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
@@ -50,4 +51,139 @@ describe("POST /admin/discounts", () => {
)
})
})
describe("unsuccessful update with dynamic discount using an invalid iso8601 duration", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
{
payload: {
code: "10TOTALOFF",
rule: {
id: "1234",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "PaMT2D",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message[0].message).toEqual(
`"valid_duration" must be a valid ISO 8601 duration`
)
})
})
describe("successful update with dynamic discount", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
{
payload: {
code: "10TOTALOFF",
rule: {
id: "1234",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service update", () => {
expect(DiscountServiceMock.update).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("total10"),
{
code: "10TOTALOFF",
rule: {
id: "1234",
type: "fixed",
value: 10,
allocation: "total",
},
starts_at: new Date("02/02/2021 13:45"),
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
}
)
})
})
describe("fails on invalid date intervals", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
{
payload: {
code: "10TOTALOFF",
rule: {
id: "1234",
type: "fixed",
value: 10,
allocation: "total",
},
ends_at: "02/02/2021",
starts_at: "03/14/2021",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message[0].message).toEqual(
`"ends_at" must be greater than "ref:starts_at"`
)
})
})
})

View File

@@ -1,4 +1,5 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations } from "."
/**
* @oas [post] /discounts
@@ -71,7 +72,13 @@ export default async (req, res) => {
.required(),
is_disabled: Validator.boolean().default(false),
starts_at: Validator.date().optional(),
ends_at: Validator.date().optional(),
ends_at: Validator.date()
.greater(Validator.ref("starts_at"))
.optional(),
valid_duration: Validator.string()
.isoDuration()
.allow(null)
.optional(),
usage_limit: Validator.number()
.positive()
.optional(),
@@ -90,11 +97,10 @@ export default async (req, res) => {
const discountService = req.scope.resolve("discountService")
const created = await discountService.create(value)
const discount = await discountService.retrieve(created.id, [
"rule",
"rule.valid_for",
"regions",
])
const discount = await discountService.retrieve(
created.id,
defaultRelations
)
res.status(200).json({ discount })
} catch (err) {

View File

@@ -37,9 +37,9 @@ export default async (req, res) => {
try {
const discountService = req.scope.resolve("discountService")
await discountService.createDynamicCode(discount_id, value)
const created = await discountService.createDynamicCode(discount_id, value)
const discount = await discountService.retrieve(discount_id, {
const discount = await discountService.retrieve(created.id, {
relations: ["rule", "rule.valid_for", "regions"],
})

View File

@@ -74,6 +74,7 @@ export const defaultFields = [
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
export const defaultRelations = [

View File

@@ -68,7 +68,16 @@ export default async (req, res) => {
.optional(),
is_disabled: Validator.boolean().optional(),
starts_at: Validator.date().optional(),
ends_at: Validator.date().optional(),
ends_at: Validator.when("starts_at", {
not: undefined,
then: Validator.date()
.greater(Validator.ref("starts_at"))
.optional(),
otherwise: Validator.date().optional(),
}),
valid_duration: Validator.string()
.isoDuration().allow(null)
.optional(),
usage_limit: Validator.number()
.positive()
.optional(),
@@ -78,6 +87,7 @@ export default async (req, res) => {
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}

View File

@@ -57,6 +57,7 @@ export const defaultCartRelations = [
"payment_sessions",
"shipping_methods.shipping_option",
"discounts",
"discounts.rule",
]
export const defaultCartFields = [

View File

@@ -43,7 +43,14 @@ export default async (req, res) => {
.withTransaction(manager)
.retrieve(draftOrder.cart_id, {
select: ["total"],
relations: ["discounts", "shipping_methods", "region", "items"],
relations: [
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"region",
"items",
],
})
await paymentProviderService

View File

@@ -7,6 +7,8 @@ const defaultRelations = [
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",
@@ -25,6 +27,7 @@ const defaultRelations = [
"claims.additional_items",
"claims.fulfillments",
"claims.claim_items",
"claims.claim_items.item",
"claims.claim_items.images",
"swaps",
"swaps.return_order",
@@ -54,6 +57,7 @@ const defaultFields = [
"metadata",
"items.refundable",
"swaps.additional_items.refundable",
"claims.additional_items.refundable",
"shipping_total",
"discount_total",
"tax_total",

View File

@@ -202,7 +202,12 @@ export default async (req, res) => {
const order = await orderService
.withTransaction(manager)
.retrieve(id, {
relations: ["items", "discounts"],
relations: [
"items",
"cart",
"cart.discounts",
"cart.discounts.rule",
],
})
await claimService.withTransaction(manager).create({

View File

@@ -45,6 +45,17 @@ import { defaultFields, defaultRelations } from "./"
* quantity:
* description: The quantity of the Product Variant to ship.
* type: integer
* custom_shipping_options:
* description: The custom shipping options to potentially create a Shipping Method from.
* type: array
* items:
* properties:
* option_id:
* description: The id of the Shipping Option to override with a custom price.
* type: string
* price:
* description: The custom price of the Shipping Option.
* type: integer
* no_notification:
* description: If set to true no notification will be send related to this Swap.
* type: boolean
@@ -85,6 +96,12 @@ export default async (req, res) => {
variant_id: Validator.string().required(),
quantity: Validator.number().required(),
}),
custom_shipping_options: Validator.array()
.items({
option_id: Validator.string().required(),
price: Validator.number().required(),
})
.default([]),
no_notification: Validator.boolean().optional(),
allow_backorder: Validator.boolean().default(true),
})
@@ -149,7 +166,9 @@ export default async (req, res) => {
}
)
await swapService.withTransaction(manager).createCart(swap.id)
await swapService
.withTransaction(manager)
.createCart(swap.id, value.custom_shipping_options)
const returnOrder = await returnService
.withTransaction(manager)
.retrieveBySwap(swap.id)

View File

@@ -221,6 +221,8 @@ export const defaultRelations = [
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",
@@ -239,6 +241,7 @@ export const defaultRelations = [
"claims.additional_items",
"claims.fulfillments",
"claims.claim_items",
"claims.claim_items.item",
"claims.claim_items.images",
// "claims.claim_items.tags",
"swaps",
@@ -269,6 +272,7 @@ export const defaultFields = [
"metadata",
"items.refundable",
"swaps.additional_items.refundable",
"claims.additional_items.refundable",
"shipping_total",
"discount_total",
"tax_total",
@@ -316,6 +320,8 @@ export const allowedRelations = [
"billing_address",
"shipping_address",
"discounts",
"discounts.rule",
"discounts.rule.valid_for",
"shipping_methods",
"payments",
"fulfillments",

View File

@@ -46,7 +46,11 @@ export default app => {
)
route.get("/:id", middlewares.wrap(require("./get-product").default))
route.get("/", middlewares.wrap(require("./list-products").default))
route.get(
"/",
middlewares.normalizeQuery(),
middlewares.wrap(require("./list-products").default)
)
return app
}
@@ -121,3 +125,18 @@ export const allowedRelations = [
"type",
"collection",
]
export const filterableFields = [
"id",
"status",
"collection_id",
"tags",
"title",
"description",
"handle",
"is_giftcard",
"type",
"created_at",
"updated_at",
"deleted_at",
]

View File

@@ -1,6 +1,6 @@
import _ from "lodash"
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultFields, defaultRelations } from "./"
import { defaultFields, defaultRelations, filterableFields } from "./"
/**
* @oas [get] /products
@@ -31,6 +31,17 @@ import { defaultFields, defaultRelations } from "./"
* $ref: "#/components/schemas/product"
*/
export default async (req, res) => {
const schema = Validator.productFilter()
const { value, error } = schema.validate(req.query)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
JSON.stringify(error.details)
)
}
try {
const productService = req.scope.resolve("productService")
@@ -53,21 +64,16 @@ export default async (req, res) => {
expandFields = req.query.expand.split(",")
}
if ("is_giftcard" in req.query) {
selector.is_giftcard = req.query.is_giftcard === "true"
for (const k of filterableFields) {
if (k in value) {
selector[k] = value[k]
}
}
if ("status" in req.query) {
const schema = Validator.array()
.items(
Validator.string().valid("proposed", "draft", "published", "rejected")
)
.single()
const { value, error } = schema.validate(req.query.status)
if (value && !error) {
selector.status = value
if (selector.status?.indexOf("null") > -1) {
selector.status.splice(selector.status.indexOf("null"), 1)
if (selector.status.length === 0) {
delete selector.status
}
}

View File

@@ -23,7 +23,7 @@ describe("POST /store/carts/:id/shipping-methods", () => {
jest.clearAllMocks()
})
it("calls CartService addShipping", () => {
it("calls CartService addShippingMethod", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
@@ -45,6 +45,50 @@ describe("POST /store/carts/:id/shipping-methods", () => {
})
})
describe("successfully adds a shipping method", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("swap-cart")
subject = await request(
"POST",
`/store/carts/${cartId}/shipping-methods`,
{
payload: {
option_id: IdMap.getId("freeShipping"),
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService addShippingMethod", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("swap-cart"),
IdMap.getId("freeShipping"),
{}
)
})
it("calls CartService retrieve", () => {
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body.cart).toEqual(
expect.objectContaining({ type: "swap", id: IdMap.getId("test-swap") })
)
})
})
describe("successfully adds a shipping method with additional data", () => {
let subject
@@ -68,7 +112,7 @@ describe("POST /store/carts/:id/shipping-methods", () => {
jest.clearAllMocks()
})
it("calls CartService addShipping", () => {
it("calls CartService addShippingMethod", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),

View File

@@ -44,7 +44,9 @@ export default async (req, res) => {
await manager.transaction(async m => {
const txCartService = cartService.withTransaction(m)
await txCartService.addShippingMethod(id, value.option_id, value.data)
const updated = await txCartService.retrieve(id, {
relations: ["payment_sessions"],
})
@@ -54,12 +56,12 @@ export default async (req, res) => {
}
})
const cart = await cartService.retrieve(id, {
const updatedCart = await cartService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ cart })
res.status(200).json({ cart: updatedCart })
} catch (err) {
throw err
}

View File

@@ -82,6 +82,14 @@ export default async (req, res) => {
if (!value.region_id) {
const regionService = req.scope.resolve("regionService")
const regions = await regionService.withTransaction(manager).list({})
if (!regions?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`A region is required to create a cart`
)
}
regionId = regions[0].id
}

Some files were not shown because too many files have changed in this diff Show More