release: next (#315)

Co-authored-by: Sebastian Mateos Nicolajsen <sebastian.m.nicolajsen@gmail.com>
Co-authored-by: Abraham Ugbeshe <abrahamugbeshe@gmail.com>
Co-authored-by: olivermrbl <oliver@mrbltech.com>
This commit is contained in:
committed 2021-07-15 09:37:27 +02:00
1 parent 44d31b4d83
commit 2585e958de
144 files changed
+2554 -240

No files matched your search

@@ -9,6 +9,9 @@ export default () => {
let statusCode = 500
switch (err.name) {
case MedusaError.Types.DUPLICATE_ERROR:
statusCode = 409
break
case MedusaError.Types.NOT_ALLOWED:
case MedusaError.Types.INVALID_DATA:
statusCode = 400
@@ -63,6 +63,9 @@ import { defaultFields, defaultRelations } from "."
* customer_id:
* description: The id of the customer to add on the draft order
* type: string
* no_notification_order:
* description: An optional flag passed to the resulting order to determine use of notifications.
* type: boolean
* shipping_methods:
* description: The shipping methods for the draft order
* type: array
@@ -123,6 +126,7 @@ export default async (req, res) => {
})
.optional(),
customer_id: Validator.string().optional(),
no_notification_order: Validator.boolean().optional(),
shipping_methods: Validator.array()
.items({
option_id: Validator.string().required(),
@@ -78,6 +78,7 @@ export const defaultFields = [
"created_at",
"updated_at",
"metadata",
"no_notification_order",
]
export const allowedFields = [
@@ -89,6 +90,7 @@ export const allowedFields = [
"created_at",
"updated_at",
"metadata",
"no_notification_order",
]
export const allowedRelations = ["cart"]
@@ -35,6 +35,9 @@ import { defaultCartFields, defaultCartRelations, defaultFields } from "."
* code:
* description: "The code that a Discount is identifed by."
* type: string
* no_notification_order:
* description: "An optional flag passed to the resulting order to determine use of notifications."
* type: boolean
* customer_id:
* description: "The id of the Customer to associate the Draft Order with."
* type: string
@@ -68,6 +71,7 @@ export default async (req, res) => {
})
.optional(),
customer_id: Validator.string().optional(),
no_notification_order: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
@@ -88,6 +92,13 @@ export default async (req, res) => {
)
}
if ("no_notification_order" in value) {
await draftOrderService.update(draftOrder.id, {
no_notification_order: value.no_notification_order,
})
delete value.no_notification_order
}
await cartService.update(draftOrder.cart_id, value)
draftOrder.cart = await cartService.retrieve(draftOrder.cart_id, {
@@ -33,7 +33,10 @@ export const defaultFields = [
"metadata",
]
export const defaultRelations = ["region"]
export const defaultRelations = [
"region",
"order",
]
export const allowedFields = [
"id",
@@ -1,3 +1,4 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultFields, defaultRelations } from "./"
/**
@@ -21,14 +22,23 @@ import { defaultFields, defaultRelations } from "./"
*/
export default async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 50
const offset = parseInt(req.query.offset) || 0
const selector = {}
if ("q" in req.query) {
selector.q = req.query.q
}
const giftCardService = req.scope.resolve("giftCardService")
const giftCards = await giftCardService.list(selector, {
select: defaultFields,
relations: defaultRelations,
order: { created_at: "DESC" },
limit: limit,
skip: offset,
})
res.status(200).json({ gift_cards: giftCards })
@@ -1,4 +1,5 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultFields, defaultRelations } from "./"
/**
* @oas [post] /gift-cards/{id}
@@ -42,7 +42,7 @@ describe("POST /admin/orders/:id/fulfillment", () => {
quantity: 1,
},
],
undefined
{ metadata: undefined, no_notification: undefined }
)
})
@@ -62,6 +62,7 @@ const defaultFields = [
"total",
"paid_total",
"refundable_amount",
"no_notification",
]
describe("GET /admin/orders", () => {
@@ -1,7 +1,7 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { orders } from "../../../../../services/__mocks__/order"
import { ReturnService } from "../../../../../services/__mocks__/return"
import { EventBusServiceMock } from "../../../../../services/__mocks__/event-bus"
describe("POST /admin/orders/:id/return", () => {
describe("successfully returns full order", () => {
@@ -21,6 +21,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund: 10,
no_notification: true,
},
adminSession: {
jwt: {
@@ -47,6 +48,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund_amount: 10,
no_notification: true,
shipping_method: undefined,
})
})
@@ -69,6 +71,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund: -1,
no_notification: true,
},
adminSession: {
jwt: {
@@ -95,6 +98,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund_amount: 0,
no_notification: true,
shipping_method: undefined,
})
})
@@ -118,6 +122,7 @@ describe("POST /admin/orders/:id/return", () => {
],
refund: -1,
},
no_notification: true,
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
@@ -143,6 +148,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund_amount: 0,
no_notification: true,
shipping_method: undefined,
})
})
@@ -165,6 +171,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund: 100,
no_notification: true,
return_shipping: {
option_id: "opt_1234",
price: 12,
@@ -195,6 +202,7 @@ describe("POST /admin/orders/:id/return", () => {
},
],
refund_amount: 100,
no_notification: true,
shipping_method: {
option_id: "opt_1234",
price: 12,
@@ -205,4 +213,82 @@ describe("POST /admin/orders/:id/return", () => {
expect(ReturnService.fulfill).toHaveBeenCalledWith("return")
})
})
describe("the api call overrides notification settings of order", () => {
it("eventBus is called with the proper no notification feature", async () => {
jest.clearAllMocks()
const subject = await request(
"POST",
`/admin/orders/${IdMap.getId("test-order")}/return`,
{
payload: {
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund: 100,
return_shipping: {
option_id: "opt_1234",
price: 12,
},
no_notification: false,
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
expect.any(String),
{
id: expect.any(String),
no_notification: false,
return_id: expect.any(String),
}
)
})
})
describe("the api call inherits notification settings of order", () => {
it("eventBus is called with the proper no notification feature", async () => {
jest.clearAllMocks()
await request(
"POST",
`/admin/orders/${IdMap.getId("test-order")}/return`,
{
payload: {
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund: 100,
return_shipping: {
option_id: "opt_1234",
price: 12,
},
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
expect.any(String),
{
id: expect.any(String),
no_notification: true,
return_id: expect.any(String),
}
)
})
})
})
@@ -91,6 +91,9 @@ import { defaultRelations, defaultFields } from "./"
* refund_amount:
* description: The amount to refund the Customer when the Claim type is `refund`.
* type: integer
* no_notification:
* description: If set to true no notification will be send related to this Claim.
* type: boolean
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
@@ -108,7 +111,6 @@ import { defaultRelations, defaultFields } from "./"
*/
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
type: Validator.string()
.valid("replace", "refund")
@@ -155,6 +157,7 @@ export default async (req, res) => {
.integer()
.optional(),
shipping_address: Validator.object().optional(),
no_notification: Validator.boolean().optional(),
metadata: Validator.object().optional(),
})
@@ -162,7 +165,6 @@ export default async (req, res) => {
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
const idempotencyKeyService = req.scope.resolve("idempotencyKeyService")
const headerKey = req.get("Idempotency-Key") || ""
@@ -212,6 +214,7 @@ export default async (req, res) => {
return_shipping: value.return_shipping,
additional_items: value.additional_items,
shipping_methods: value.shipping_methods,
no_notification: value.no_notification,
metadata: value.metadata,
})
@@ -24,6 +24,9 @@ import { defaultRelations, defaultFields } from "./"
* quantity:
* description: The quantity of the Line Item to fulfill.
* type: integer
* no_notification:
* description: If set to true no notification will be send related to this Swap.
* type: boolean
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
@@ -49,6 +52,7 @@ export default async (req, res) => {
quantity: Validator.number().required(),
})
.required(),
no_notification: Validator.boolean().optional(),
metadata: Validator.object().optional(),
})
@@ -60,7 +64,10 @@ export default async (req, res) => {
try {
const orderService = req.scope.resolve("orderService")
await orderService.createFulfillment(id, value.items, value.metadata)
await orderService.createFulfillment(id, value.items, {
metadata: value.metadata,
no_notification: value.no_notification,
})
const order = await orderService.retrieve(id, {
select: defaultFields,
@@ -27,6 +27,7 @@ export default async (req, res) => {
items: Validator.array().optional(),
})
.required(),
no_notification: Validator.boolean().optional(),
metadata: Validator.object().optional(),
})
@@ -21,6 +21,9 @@ import { defaultRelations, defaultFields } from "./"
* type: array
* items:
* type: string
* no_notification:
* description: If set to true no notification will be send related to this Shipment.
* type: boolean
* tags:
* - Order
* responses:
@@ -41,6 +44,7 @@ export default async (req, res) => {
tracking_numbers: Validator.array()
.items(Validator.string())
.optional(),
no_notification: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
@@ -54,7 +58,8 @@ export default async (req, res) => {
await orderService.createShipment(
id,
value.fulfillment_id,
value.tracking_numbers.map(n => ({ tracking_number: n }))
value.tracking_numbers.map(n => ({ tracking_number: n })),
{ no_notification: value.no_notification }
)
const order = await orderService.retrieve(id, {
@@ -22,6 +22,9 @@ import { defaultFields, defaultRelations } from "./"
* type: array
* items:
* type: string
* no_notification:
* description: If set to true no notification will be send related to this Claim.
* type: boolean
* tags:
* - Order
* responses:
@@ -42,6 +45,7 @@ export default async (req, res) => {
tracking_numbers: Validator.array()
.items(Validator.string())
.optional(),
no_notification: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
@@ -56,7 +60,8 @@ export default async (req, res) => {
await swapService.createShipment(
swap_id,
value.fulfillment_id,
value.tracking_numbers.map(n => ({ tracking_number: n }))
value.tracking_numbers.map(n => ({ tracking_number: n })),
{ no_notification: value.no_notification }
)
const order = await orderService.retrieve(id, {
@@ -45,6 +45,9 @@ import { defaultFields, defaultRelations } from "./"
* quantity:
* description: The quantity of the Product Variant to ship.
* type: integer
* no_notification:
* description: If set to true no notification will be send related to this Swap.
* type: boolean
* tags:
* - Order
* responses:
@@ -79,6 +82,7 @@ export default async (req, res) => {
variant_id: Validator.string().required(),
quantity: Validator.number().required(),
}),
no_notification: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
@@ -134,7 +138,10 @@ export default async (req, res) => {
value.return_items,
value.additional_items,
value.return_shipping,
{ idempotency_key: idempotencyKey.idempotency_key }
{
idempotency_key: idempotencyKey.idempotency_key,
no_notification: value.no_notification,
}
)
await swapService.withTransaction(manager).createCart(swap.id)
@@ -17,6 +17,9 @@ import { defaultRelations, defaultFields } from "./"
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* no_notification:
* description: If set to true no notification will be send related to this Claim.
* type: boolean
* tags:
* - Order
* responses:
@@ -34,6 +37,7 @@ export default async (req, res) => {
const schema = Validator.object().keys({
metadata: Validator.object().optional(),
no_notification: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
@@ -47,9 +51,10 @@ export default async (req, res) => {
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
await claimService
.withTransaction(manager)
.createFulfillment(claim_id, value.metadata)
await claimService.withTransaction(manager).createFulfillment(claim_id, {
metadata: value.metadata,
no_notification: value.no_notification,
})
})
const order = await orderService.retrieve(id, {
@@ -17,6 +17,9 @@ import { defaultRelations, defaultFields } from "./"
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* no_notification:
* description: If set to true no notification will be send related to this Claim.
* type: boolean
* tags:
* - Order
* responses:
@@ -34,6 +37,7 @@ export default async (req, res) => {
const schema = Validator.object().keys({
metadata: Validator.object().optional(),
no_notification: Validator.boolean().optional,
})
const { value, error } = schema.validate(req.body)
@@ -47,9 +51,10 @@ export default async (req, res) => {
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
await swapService
.withTransaction(manager)
.createFulfillment(swap_id, value.metadata)
await swapService.withTransaction(manager).createFulfillment(swap_id, {
metadata: value.metadata,
no_notification: value.no_notification,
})
const order = await orderService.withTransaction(manager).retrieve(id, {
select: defaultFields,
@@ -237,6 +237,7 @@ export const defaultFields = [
"total",
"paid_total",
"refundable_amount",
"no_notification",
]
export const allowedFields = [
@@ -265,6 +266,7 @@ export const allowedFields = [
"total",
"paid_total",
"refundable_amount",
"no_notification",
]
export const allowedRelations = [
@@ -25,6 +25,9 @@ import { defaultRelations, defaultFields } from "./"
* note:
* description: A not with additional details about the Refund.
* type: string
* no_notification:
* description: If set to true no notification will be send related to this Refund.
* type: boolean
* tags:
* - Order
* responses:
@@ -47,9 +50,11 @@ export default async (req, res) => {
note: Validator.string()
.allow("")
.optional(),
no_notification: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
@@ -57,7 +62,13 @@ export default async (req, res) => {
try {
const orderService = req.scope.resolve("orderService")
await orderService.createRefund(id, value.amount, value.reason, value.note)
await orderService.createRefund(
id,
value.amount,
value.reason,
value.note,
{ no_notification: value.no_notification }
)
const order = await orderService.retrieve(id, {
select: defaultFields,
@@ -43,6 +43,9 @@ import { defaultRelations, defaultFields } from "./"
* receive_now:
* description: A flag to indicate if the Return should be registerd as received immediately.
* type: boolean
* no_notification:
* description: A flag to indicate if no notifications should be emitted related to the requested Return.
* type: boolean
* refund:
* description: The amount to refund.
* type: integer
@@ -79,6 +82,7 @@ export default async (req, res) => {
})
.optional(),
receive_now: Validator.boolean().default(false),
no_notification: Validator.boolean().optional(),
refund: Validator.number()
.integer()
.optional(),
@@ -141,6 +145,13 @@ export default async (req, res) => {
}
}
let order = await orderService
.withTransaction(manager)
.retrieve(id)
const evaluatedNoNotification = value.no_notification !== undefined ? value.no_notification : order.no_notification
returnObj.no_notification = evaluatedNoNotification
const createdReturn = await returnService
.withTransaction(manager)
.create(returnObj)
@@ -150,12 +161,13 @@ export default async (req, res) => {
.withTransaction(manager)
.fulfill(createdReturn.id)
}
await eventBus
.withTransaction(manager)
.emit("order.return_requested", {
id,
return_id: createdReturn.id,
no_notification: evaluatedNoNotification
})
return {
@@ -62,6 +62,9 @@ import { defaultRelations, defaultFields } from "./"
* price:
* description: The price to charge for the Shipping Method
* type: integer
* no_notification:
* description: If set to true no notification will be send related to this Swap.
* type: boolean
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
@@ -106,6 +109,7 @@ export default async (req, res) => {
.optional(),
})
.optional(),
no_notification: Validator.boolean().optional(),
metadata: Validator.object().optional(),
})
@@ -23,6 +23,7 @@ export default async (req, res) => {
data: Validator.object(),
items: Validator.array(),
}),
no_notification: Validator.boolean(),
})
const { value, error } = schema.validate(req.body)
@@ -54,6 +54,7 @@ export default async (req, res) => {
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
@@ -1,6 +1,7 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
import { defaultFields, defaultRelations } from "../"
describe("POST /store/customers", () => {
describe("successfully creates a customer", () => {
@@ -34,7 +35,7 @@ describe("POST /store/customers", () => {
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("lebron"),
{ relations: ["shipping_addresses"] }
{ relations: defaultRelations, select: defaultFields }
)
})
@@ -1,5 +1,6 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { defaultFields, defaultRelations } from "../"
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
describe("POST /store/customers/:id", () => {
@@ -42,7 +43,7 @@ describe("POST /store/customers/:id", () => {
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("lebron"),
{ relations: ["shipping_addresses"] }
{ relations: defaultRelations, select: defaultFields }
)
})
@@ -55,6 +56,98 @@ describe("POST /store/customers/:id", () => {
})
})
describe("successfully updates a customer with billing address id", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/customers/${IdMap.getId("lebron")}`,
{
payload: {
billing_address: "test",
},
clientSession: {
jwt: {
customer_id: IdMap.getId("lebron"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CustomerService update", () => {
expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("lebron"),
{
billing_address: "test",
}
)
})
it("status code 200", () => {
expect(subject.status).toEqual(200)
})
})
describe("successfully updates a customer with billing address object", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/customers/${IdMap.getId("lebron")}`,
{
payload: {
billing_address: {
first_name: "Olli",
last_name: "Juhl",
address_1: "Laksegade",
city: "Copenhagen",
country_code: "dk",
postal_code: "2100",
phone: "+1 (222) 333 4444",
},
},
clientSession: {
jwt: {
customer_id: IdMap.getId("lebron"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CustomerService update", () => {
expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("lebron"),
{
billing_address: {
first_name: "Olli",
last_name: "Juhl",
address_1: "Laksegade",
city: "Copenhagen",
country_code: "dk",
postal_code: "2100",
phone: "+1 (222) 333 4444",
},
}
)
})
it("status code 200", () => {
expect(subject.status).toEqual(200)
})
})
describe("fails if not authenticated", () => {
let subject
beforeAll(async () => {
@@ -1,4 +1,5 @@
import { Validator, MedusaError } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /customers/{id}/addresses
@@ -45,7 +46,8 @@ export default async (req, res) => {
let customer = await customerService.addAddress(id, value.address)
customer = await customerService.retrieve(id, {
relations: ["shipping_addresses"],
relations: defaultRelations,
select: defaultFields,
})
res.status(200).json({ customer })
@@ -1,6 +1,7 @@
import jwt from "jsonwebtoken"
import { Validator, MedusaError } from "medusa-core-utils"
import config from "../../../../config"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /customers
@@ -40,6 +41,7 @@ export default async (req, res) => {
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const customerService = req.scope.resolve("customerService")
let customer = await customerService.create(value)
@@ -50,7 +52,8 @@ export default async (req, res) => {
})
customer = await customerService.retrieve(customer.id, {
relations: ["shipping_addresses"],
relations: defaultRelations,
select: defaultFields,
})
res.status(200).json({ customer })
@@ -1,3 +1,5 @@
import { defaultRelations, defaultFields } from "./"
/**
* @oas [delete] /customers/{id}/addresses/{address_id}
* operationId: DeleteCustomersCustomerAddressesAddress
@@ -25,7 +27,8 @@ export default async (req, res) => {
try {
await customerService.removeAddress(id, address_id)
customer = await customerService.retrieve(id, {
relations: ["shipping_addresses"],
relations: defaultRelations,
select: defaultFields,
})
res.json({ customer })
@@ -1,3 +1,5 @@
import { defaultRelations, defaultFields } from "./"
/**
* @oas [get] /customers/{id}
* operationId: GetCustomersCustomer
@@ -22,7 +24,8 @@ export default async (req, res) => {
try {
const customerService = req.scope.resolve("customerService")
const customer = await customerService.retrieve(id, {
relations: ["shipping_addresses"],
relations: defaultRelations,
select: defaultFields,
})
res.json({ customer })
} catch (err) {
@@ -57,3 +57,39 @@ export default (app, container) => {
return app
}
export const defaultRelations = ["shipping_addresses", "billing_address"]
export const defaultFields = [
"id",
"email",
"first_name",
"last_name",
"billing_address_id",
"phone",
"has_account",
"created_at",
"updated_at",
"deleted_at",
"metadata",
]
export const allowedRelations = [
"shipping_addresses",
"billing_address",
"orders",
]
export const allowedFields = [
"id",
"email",
"first_name",
"last_name",
"billing_address_id",
"phone",
"has_account",
"created_at",
"updated_at",
"deleted_at",
"metadata",
]
@@ -1,4 +1,5 @@
import { Validator, MedusaError } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /customers/{id}/addresses/{address_id}
@@ -50,7 +51,8 @@ export default async (req, res) => {
)
customer = await customerService.retrieve(id, {
relations: ["shipping_addresses"],
relations: defaultRelations,
select: defaultFields,
})
res.json({ customer })
@@ -1,4 +1,5 @@
import { Validator, MedusaError } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /customers/{id}
@@ -18,12 +19,19 @@ import { Validator, MedusaError } from "medusa-core-utils"
* last_name:
* description: "The Customer's last name."
* type: string
* billing_address:
* description: "The Address to be used for billing purposes."
* anyOf:
* - $ref: "#/components/schemas/address"
* password:
* description: "The Customer's password."
* type: string
* phone:
* description: "The Customer's phone number."
* type: string
* metadata:
* description: "Metadata about the customer."
* type: object
* tags:
* - Customer
* responses:
@@ -40,10 +48,12 @@ export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
billing_address: Validator.address().optional(),
first_name: Validator.string().optional(),
last_name: Validator.string().optional(),
password: Validator.string().optional(),
phone: Validator.string().optional(),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
@@ -56,7 +66,8 @@ export default async (req, res) => {
let customer = await customerService.update(id, value)
customer = await customerService.retrieve(customer.id, {
relations: ["shipping_addresses"],
relations: defaultRelations,
select: defaultFields,
})
res.status(200).json({ customer })
@@ -30,6 +30,7 @@ export default app => {
export const defaultRelations = [
"shipping_address",
"fulfillments",
"fulfillments.tracking_links",
"items",
"items.variant",
"items.variant.product",
@@ -66,6 +67,7 @@ export const defaultFields = [
export const allowedRelations = [
"shipping_address",
"fulfillments",
"fulfillments.tracking_links",
"billing_address",
"items",
"items.variant",
+1
View File
@@ -43,3 +43,4 @@ export { Swap } from "./models/swap"
export { User } from "./models/user"
export { DraftOrder } from "./models/draft-order"
export { ReturnReason } from "./models/return-reason"
@@ -0,0 +1,25 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class noNotification1623231564533 implements MigrationInterface {
name = 'noNotification1623231564533'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "return" ADD "no_notification" boolean`);
await queryRunner.query(`ALTER TABLE "claim_order" ADD "no_notification" boolean`);
await queryRunner.query(`ALTER TABLE "swap" ADD "no_notification" boolean`);
await queryRunner.query(`ALTER TABLE "order" ADD "no_notification" boolean`);
await queryRunner.query(`ALTER TABLE "draft_order" ADD "no_notification_order" boolean`);
await queryRunner.query(`ALTER TABLE "fulfillment" ADD "no_notification" boolean`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "fulfillment" DROP COLUMN "no_notification"`);
await queryRunner.query(`ALTER TABLE "draft_order" DROP COLUMN "no_notification_order"`);
await queryRunner.query(`ALTER TABLE "order" DROP COLUMN "no_notification"`);
await queryRunner.query(`ALTER TABLE "swap" DROP COLUMN "no_notification"`);
await queryRunner.query(`ALTER TABLE "claim_order" DROP COLUMN "no_notification"`);
await queryRunner.query(`ALTER TABLE "return" DROP COLUMN "no_notification"`);
}
}
@@ -4,28 +4,16 @@ export class gcRemoveUniqueOrder1624287602631 implements MigrationInterface {
name = "gcRemoveUniqueOrder1624287602631"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "gift_card" DROP CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0"`
)
await queryRunner.query(`ALTER TABLE "gift_card" DROP CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0"`)
await queryRunner.query(`COMMENT ON COLUMN "gift_card"."order_id" IS NULL`)
await queryRunner.query(
`ALTER TABLE "gift_card" DROP CONSTRAINT "REL_dfc1f02bb0552e79076aa58dbb"`
)
await queryRunner.query(
`ALTER TABLE "gift_card" ADD CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(`ALTER TABLE "gift_card" DROP CONSTRAINT "REL_dfc1f02bb0552e79076aa58dbb"`)
await queryRunner.query(`ALTER TABLE "gift_card" ADD CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "gift_card" DROP CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0"`
)
await queryRunner.query(
`ALTER TABLE "gift_card" ADD CONSTRAINT "REL_dfc1f02bb0552e79076aa58dbb" UNIQUE ("order_id")`
)
await queryRunner.query(`ALTER TABLE "gift_card" DROP CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0"`)
await queryRunner.query(`ALTER TABLE "gift_card" ADD CONSTRAINT "REL_dfc1f02bb0552e79076aa58dbb" UNIQUE ("order_id")`)
await queryRunner.query(`COMMENT ON COLUMN "gift_card"."order_id" IS NULL`)
await queryRunner.query(
`ALTER TABLE "gift_card" ADD CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(`ALTER TABLE "gift_card" ADD CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
}
}
@@ -0,0 +1,42 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class softDeletingUniqueConstraints1624610325746 implements MigrationInterface {
name = 'softDeletingUniqueConstraints1624610325746'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_6910923cb678fd6e99011a21cc"`);
await queryRunner.query(`DROP INDEX "IDX_db7355f7bd36c547c8a4f539e5"`);
await queryRunner.query(`DROP INDEX "IDX_087926f6fec32903be3c8eedfa"`);
await queryRunner.query(`DROP INDEX "IDX_f4dc2c0888b66d547c175f090e"`);
await queryRunner.query(`DROP INDEX "IDX_9db95c4b71f632fc93ecbc3d8b"`);
await queryRunner.query(`DROP INDEX "IDX_7124082c8846a06a857cca386c"`);
await queryRunner.query(`DROP INDEX "IDX_a0a3f124dc5b167622217fee02"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e08af711f3493df1e921c4c9ef" ON "product_collection" ("handle") WHERE deleted_at IS NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_77c4073c30ea7793f484750529" ON "product" ("handle") WHERE deleted_at IS NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae3e22c67d7c7a969a363533c0" ON "discount" ("code") WHERE deleted_at IS NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0683952543d7d3f4fffc427034" ON "product_variant" ("sku") WHERE deleted_at IS NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_410649600ce31c10c4b667ca10" ON "product_variant" ("barcode") WHERE deleted_at IS NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5248fda27b9f16ef818604bb6f" ON "product_variant" ("ean") WHERE deleted_at IS NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_832f86daf8103491d634a967da" ON "product_variant" ("upc") WHERE deleted_at IS NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_ae3e22c67d7c7a969a363533c0"`);
await queryRunner.query(`DROP INDEX "IDX_77c4073c30ea7793f484750529"`);
await queryRunner.query(`DROP INDEX "IDX_e08af711f3493df1e921c4c9ef"`);
await queryRunner.query(`DROP INDEX "IDX_832f86daf8103491d634a967da"`);
await queryRunner.query(`DROP INDEX "IDX_5248fda27b9f16ef818604bb6f"`);
await queryRunner.query(`DROP INDEX "IDX_410649600ce31c10c4b667ca10"`);
await queryRunner.query(`DROP INDEX "IDX_0683952543d7d3f4fffc427034"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_087926f6fec32903be3c8eedfa" ON "discount" ("code") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_db7355f7bd36c547c8a4f539e5" ON "product" ("handle") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6910923cb678fd6e99011a21cc" ON "product_collection" ("handle") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0a3f124dc5b167622217fee02" ON "product_variant" ("upc") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7124082c8846a06a857cca386c" ON "product_variant" ("ean") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9db95c4b71f632fc93ecbc3d8b" ON "product_variant" ("barcode") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f4dc2c0888b66d547c175f090e" ON "product_variant" ("sku") `);
}
}
+1 -1
View File
@@ -246,7 +246,7 @@ export class Cart {
@Column({ type: "jsonb", nullable: true })
context: any
// Total fields
shipping_total: number
discount_total: number
@@ -134,6 +134,9 @@ export class ClaimOrder {
@DeleteDateColumn({ type: "timestamptz" })
deleted_at: Date
@Column({ type: "boolean", nullable: true})
no_notification: Boolean
@Column({ type: "jsonb", nullable: true })
metadata: any
@@ -223,6 +226,9 @@ export class ClaimOrder {
* deleted_at:
* type: string
* format: date-time
* no_notification:
* description: "Flag for describing whether or not notifications related to this should be send."
* type: boolean
* metadata:
* type: object
*/
+1 -1
View File
@@ -23,7 +23,7 @@ export class Discount {
@PrimaryColumn()
id: string
@Index({ unique: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
@Column()
code: string
@@ -61,6 +61,9 @@ export class DraftOrder {
@Column({ type: "timestamptz", nullable: true })
completed_at: Date
@Column({ nullable: true})
no_notification_order: boolean
@Column({ type: "jsonb", nullable: true })
metadata: any
@@ -115,6 +118,8 @@ export class DraftOrder {
* completed_at:
* type: string
* format: date-time
* no_notification_order:
* type: boolean
* metadata:
* type: object
* idempotency_key:
@@ -62,6 +62,9 @@ export class Fulfillment {
@JoinColumn({ name: "order_id" })
order: Order
@Column({ type: "boolean", nullable: true})
no_notification: Boolean
@Index()
@Column()
provider_id: string
@@ -157,6 +160,9 @@ export class Fulfillment {
* description: "The date with timezone at which the Fulfillment was shipped."
* type: string
* format: date-time
* no_notification:
* description: "Flag for describing whether or not notifications related to this should be send."
* type: boolean
* canceled_at:
* description: "The date with timezone at which the Fulfillment was canceled."
* type: string
+6
View File
@@ -245,6 +245,9 @@ export class Order {
@Column({ type: "jsonb", nullable: true })
metadata: any
@Column({ type: "boolean", nullable: true})
no_notification: Boolean
@Column({ nullable: true })
idempotency_key: string
@@ -414,4 +417,7 @@ export class Order {
* type: integer
* paid_total:
* type: integer
* no_notification:
* description: "Flag for describing whether or not notifications related to this should be send."
* type: boolean
*/
@@ -23,7 +23,7 @@ export class ProductCollection {
@Column()
title: string
@Index({ unique: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
@Column({ nullable: true })
handle: string
@@ -47,19 +47,19 @@ export class ProductVariant {
prices: MoneyAmount[]
@Column({ nullable: true })
@Index({ unique: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
sku: string
@Index({ unique: true })
@Column({ nullable: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
barcode: string
@Index({ unique: true })
@Column({ nullable: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
ean: string
@Index({ unique: true })
@Column({ nullable: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
upc: string
@Column({ type: "int" })
+1 -1
View File
@@ -39,7 +39,7 @@ export class Product {
@Column({ nullable: true })
description: string
@Index({ unique: true })
@Index({ unique: true, where: "deleted_at IS NOT NULL" })
@Column({ nullable: true })
handle: string
+6
View File
@@ -99,6 +99,9 @@ export class Return {
@UpdateDateColumn({ type: "timestamptz" })
updated_at: Date
@Column({ type: "boolean", nullable: true})
no_notification: Boolean
@Column({ type: "jsonb", nullable: true })
metadata: any
@@ -165,6 +168,9 @@ export class Return {
* description: "The date with timezone at which the resource was last updated."
* type: string
* format: date-time
* no_notification:
* description: "When set to true, no notification will be sent related to this return."
* type: boolean
* metadata:
* description: "An optional key-value map with additional information."
* type: object
+6
View File
@@ -130,6 +130,9 @@ export class Swap {
@DeleteDateColumn({ type: "timestamptz" })
deleted_at: Date
@Column({ type: "boolean", nullable: true})
no_notification: Boolean
@Column({ type: "jsonb", nullable: true })
metadata: any
@@ -224,6 +227,9 @@ export class Swap {
* description: "The date with timezone at which the resource was last updated."
* type: string
* format: date-time
* no_notification:
* description: "If set to true, no notification will be sent related to this swap"
* type: boolean
* metadata:
* description: "An optional key-value map with additional information."
* type: object
+58 -2
View File
@@ -1,5 +1,61 @@
import { EntityRepository, Repository } from "typeorm"
import { flatten, groupBy, map, merge } from "lodash"
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
import { GiftCard } from "../models/gift-card"
@EntityRepository(GiftCard)
export class GiftCardRepository extends Repository<GiftCard> {}
export class GiftCardRepository extends Repository<GiftCard> {
public async findWithRelations(
relations: Array<keyof GiftCard> = [],
idsOrOptionsWithoutRelations: Omit<
FindManyOptions<GiftCard>,
"relations"
> = {}
): Promise<GiftCard[]> {
let entities
if (Array.isArray(idsOrOptionsWithoutRelations)) {
entities = await this.findByIds(idsOrOptionsWithoutRelations)
} else {
entities = await this.find(idsOrOptionsWithoutRelations)
}
const entitiesIds = entities.map(({ id }) => id)
const groupedRelations = {}
for (const rel of relations) {
const [topLevel] = rel.split(".")
if (groupedRelations[topLevel]) {
groupedRelations[topLevel].push(rel)
} else {
groupedRelations[topLevel] = [rel]
}
}
const entitiesIdsWithRelations = await Promise.all(
Object.entries(groupedRelations).map(([_, rels]) => {
return this.findByIds(entitiesIds, {
select: ["id"],
relations: rels as string[],
})
})
).then(flatten)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entitiesAndRelationsById, entityAndRelations =>
merge({}, ...entityAndRelations)
)
}
public async findOneWithRelations(
relations: Array<keyof GiftCard> = [],
optionsWithoutRelations: Omit<FindManyOptions<GiftCard>, "relations"> = {}
): Promise<GiftCard> {
// Limit 1
optionsWithoutRelations.take = 1
const result = await this.findWithRelations(
relations,
optionsWithoutRelations
)
return result[0]
}
}
@@ -47,6 +47,7 @@ export const orders = {
providerid: "default_provider",
data: {},
},
no_notification: true,
shipping_method: [
{
providerid: "default_provider",
@@ -99,6 +100,7 @@ export const orders = {
payment_method: {
providerid: "default_provider",
},
no_notification: false,
shipping_methods: [
{
id: IdMap.getId("expensiveShipping"),
@@ -22,6 +22,7 @@ describe("ClaimService", () => {
order: {
id: "1234",
region_id: "order_region",
no_notification: true,
items: [
{
id: "itm_1",
@@ -113,6 +114,7 @@ describe("ClaimService", () => {
quantity: 1,
},
],
no_notification: true,
})
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
@@ -138,6 +140,7 @@ describe("ClaimService", () => {
expect(claimRepo.create).toHaveBeenCalledTimes(1)
expect(claimRepo.create).toHaveBeenCalledWith({
payment_status: "not_refunded",
no_notification: true,
refund_amount: 1000,
type: "refund",
order_id: "1234",
@@ -156,6 +159,7 @@ describe("ClaimService", () => {
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith("claim.created", {
id: "claim_134",
no_notification: true,
})
})
@@ -208,6 +212,25 @@ describe("ClaimService", () => {
})
).rejects.toThrow(`Claims must have at least one claim item.`)
})
it.each(
[
[false, false],
[undefined, true],
],
"passes correct no_notification status to event bus",
async (input, expected) => {
await claimService.create({
...testClaim,
no_notification: input,
})
expect(eventBusService.emit).toHaveBeenCalledWith(expect.any(String), {
id: expect.any(String),
no_notification: expected,
})
}
)
})
describe("retrieve", () => {
@@ -294,7 +317,9 @@ describe("ClaimService", () => {
})
it("successfully creates fulfillment", async () => {
await claimService.createFulfillment("claim_id", { meta: "data" })
await claimService.createFulfillment("claim_id", {
metadata: { meta: "data" },
})
expect(withTransactionMock).toHaveBeenCalledTimes(3)
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
@@ -414,7 +439,10 @@ describe("ClaimService", () => {
)
await claimService.createShipment("claim", "ful_123", ["track1234"], {
meta: "data",
metadata: {
meta: "data",
},
no_notification: false,
})
expect(withTransactionMock).toHaveBeenCalledTimes(3)
@@ -426,7 +454,12 @@ describe("ClaimService", () => {
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
"ful_123",
["track1234"],
{ meta: "data" }
{
metadata: {
meta: "data",
},
no_notification: false,
}
)
expect(lineItemService.update).toHaveBeenCalledTimes(1)
@@ -168,8 +168,14 @@ describe("CustomerService", () => {
},
})
const addressRepository = MockRepository({
create: data => data,
save: data => Promise.resolve(data),
})
const customerService = new CustomerService({
manager: MockManager,
addressRepository,
customerRepository,
eventBusService,
})
@@ -233,7 +239,7 @@ describe("CustomerService", () => {
last_name: "Juhl",
address_1: "Laksegade",
city: "Copenhagen",
country_code: "DK",
country_code: "dk",
postal_code: "2100",
phone: "+1 (222) 333 4444",
},
@@ -1,5 +1,6 @@
import _ from "lodash"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import { MockRepository, MockManager } from "medusa-test-utils"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import DraftOrderService from "../draft-order"
const eventBusService = {
@@ -205,4 +206,93 @@ describe("DraftOrderService", () => {
}
})
})
describe("update", () => {
const testOrder = {
region_id: "test-region",
shipping_address_id: "test-shipping",
billing_address_id: "test-billing",
customer_id: "test-customer",
items: [{ variant_id: "test-variant", quantity: 2, metadata: {} }],
shipping_methods: [
{
option_id: "test-option",
data: {},
},
],
}
const completedOrder = {
status: "completed",
...testOrder,
}
const draftOrderRepository = MockRepository({
create: d => ({
...d,
}),
save: d => ({
id: "test-draft-order",
...d,
}),
findOne: q => {
switch (q.where.id) {
case "completed":
return Promise.resolve(completedOrder)
default:
return Promise.resolve(testOrder)
}
},
})
const draftOrderService = new DraftOrderService({
manager: MockManager,
regionService: undefined,
cartService: undefined,
shippingOptionService: undefined,
lineItemService: undefined,
productVariantService: undefined,
draftOrderRepository,
addressRepository: undefined,
eventBusService: EventBusServiceMock,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("calls draftOrder model functions", async () => {
await draftOrderService.update("test-draft-order", {
no_notification_order: true,
})
expect(draftOrderRepository.save).toHaveBeenCalledTimes(1)
expect(draftOrderRepository.save).toHaveBeenCalledWith({
no_notification_order: true,
billing_address_id: "test-billing",
customer_id: "test-customer",
items: [
{
metadata: {},
quantity: 2,
variant_id: "test-variant",
},
],
region_id: "test-region",
shipping_address_id: "test-shipping",
shipping_methods: [
{
data: {},
option_id: "test-option",
},
],
})
})
it("fails to update draftOrder when already complete", async () => {
await expect(draftOrderService.update("completed", {})).rejects.toThrow(
"Can't update a draft order which is complete"
)
})
})
})
@@ -0,0 +1,256 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import GiftCardService from "../gift-card"
describe("GiftCardService", () => {
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
return this
},
}
describe("create", () => {
const giftCardRepo = MockRepository({
create: s => {
return Promise.resolve(s)
},
save: s => {
return Promise.resolve(s)
},
})
const regionService = {
withTransaction: function() {
return this
},
retrieve: () => {
return Promise.resolve({
id: IdMap.getId("region-id"),
})
},
}
const giftCardService = new GiftCardService({
manager: MockManager,
giftCardRepository: giftCardRepo,
regionService: regionService,
eventBusService: eventBusService,
})
const giftCard = {
region_id: IdMap.getId("region-id"),
order_id: IdMap.getId("order-id"),
is_disabled: true,
}
beforeEach(async () => {
jest.clearAllMocks()
})
it("correctly creates a giftcard", async () => {
await giftCardService.create(giftCard)
expect(giftCardRepo.create).toHaveBeenCalledTimes(1)
expect(giftCardRepo.create).toHaveBeenCalledWith({
region_id: IdMap.getId("region-id"),
order_id: IdMap.getId("order-id"),
is_disabled: true,
code: expect.any(String),
})
})
it("fails to create giftcard if no region is provided", async () => {
const card = {
...giftCard,
}
card.region_id = undefined
await expect(giftCardService.create(card)).rejects.toThrow(
"Gift card is missing region_id"
)
})
})
describe("retrieve", () => {
const giftCardRepo = MockRepository({
findOneWithRelations: () => {
return Promise.resolve({})
},
})
beforeEach(async () => {
jest.clearAllMocks()
})
const giftCardService = new GiftCardService({
manager: MockManager,
giftCardRepository: giftCardRepo,
})
it("it calls order model functions", async () => {
await giftCardService.retrieve(IdMap.getId("gift-card"), {
relations: ["region"],
select: ["id"],
})
expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledWith(
["region"],
{
where: {
id: IdMap.getId("gift-card"),
},
select: ["id"],
}
)
})
})
describe("retrieveByCode", () => {
const giftCardRepo = MockRepository({
findOneWithRelations: () => {
return Promise.resolve({})
},
})
beforeEach(async () => {
jest.clearAllMocks()
})
const giftCardService = new GiftCardService({
manager: MockManager,
giftCardRepository: giftCardRepo,
})
it("it calls order model functions", async () => {
await giftCardService.retrieveByCode("1234-1234-1234-1234", {
relations: ["region"],
select: ["id"],
})
expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledWith(
["region"],
{
where: {
code: "1234-1234-1234-1234",
},
select: ["id"],
}
)
})
})
describe("update", () => {
const giftCard = {
region_id: IdMap.getId("region-id"),
order_id: IdMap.getId("order-id"),
is_disabled: true,
value: 5000,
}
const giftCardRepo = MockRepository({
findOneWithRelations: s => {
return Promise.resolve(giftCard)
},
save: s => {
return Promise.resolve(s)
},
})
const regionService = {
withTransaction: function() {
return this
},
retrieve: () => {
return Promise.resolve({
id: IdMap.getId("other-region"),
})
},
}
const giftCardService = new GiftCardService({
manager: MockManager,
giftCardRepository: giftCardRepo,
regionService: regionService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("calls order model functions", async () => {
await giftCardService.update(IdMap.getId("giftcard-id"), {
is_disabled: false,
region_id: IdMap.getId("other-region"),
})
expect(giftCardRepo.save).toHaveBeenCalledTimes(1)
expect(giftCardRepo.save).toHaveBeenCalledWith({
region_id: IdMap.getId("other-region"),
order_id: IdMap.getId("order-id"),
is_disabled: false,
value: 5000,
})
})
it.each([[-100], [6000]])(
"fails to update balance with illegal input '%s'",
async input => {
await expect(
giftCardService.update(IdMap.getId("giftcard-id"), {
balance: input,
})
).rejects.toThrow("new balance is invalid")
}
)
})
describe("delete", () => {
const giftCard = {
region_id: IdMap.getId("region-id"),
order_id: IdMap.getId("order-id"),
}
const giftCardRepo = MockRepository({
findOne: s => {
switch (s.where.id) {
case IdMap.getId("gift-card"):
return Promise.resolve(giftCard)
default:
return Promise.resolve()
}
},
softRemove: s => {
return Promise.resolve()
},
})
const giftCardService = new GiftCardService({
manager: MockManager,
giftCardRepository: giftCardRepo,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully deletes existing gift-card", async () => {
await giftCardService.delete(IdMap.getId("gift-card"))
expect(giftCardRepo.softRemove).toHaveBeenCalledTimes(1)
expect(giftCardRepo.softRemove).toHaveBeenCalledWith({
region_id: IdMap.getId("region-id"),
order_id: IdMap.getId("order-id"),
})
})
it("returns if no gift-card found", async () => {
await giftCardService.delete(IdMap.getId("other"))
expect(giftCardRepo.softRemove).toHaveBeenCalledTimes(0)
})
})
})
@@ -1,30 +1,31 @@
import NotificationService from "../notification"
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { MockManager, MockRepository } from "medusa-test-utils"
describe("NotificationService", () => {
describe("send", () => {
const notificationRepository = MockRepository({ create: c => c })
const notificationRepository = MockRepository({ create: c => c })
const container = {
manager: MockManager,
notificationRepository,
noti_test: {
sendNotification: jest.fn(() =>
Promise.resolve({
to: "test@mail.com",
data: { id: "something" },
})
),
},
}
const container = {
manager: MockManager,
notificationRepository,
noti_test: {
sendNotification: jest.fn(() =>
Promise.resolve({
to: "test@mail.com",
data: { id: "something" },
})
),
},
}
const notificationService = new NotificationService(container)
beforeEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
jest.clearAllMocks()
})
describe("send", () =>{
it("successfully calls provider and saves noti", async () => {
const notificationService = new NotificationService(container)
await notificationService.send("event.test", { id: "test" }, "test")
expect(container.noti_test.sendNotification).toHaveBeenCalledTimes(1)
@@ -51,4 +52,30 @@ describe("NotificationService", () => {
expect(notificationRepository.save).toHaveBeenCalledWith(constructed)
})
})
describe("handleEvent", () => {
it("cancels notification if no_notification is set", async () => {
const notificationService = new NotificationService(container)
const event = "event.test"
notificationService.subscribe(event, "test")
await notificationService.handleEvent(event, {id: "id",
return_id: "id",
no_notification: true})
expect(container.noti_test.sendNotification).not.toHaveBeenCalled()
})
it("if no_notification is not set notification is send", async () => {
const notificationService = new NotificationService(container)
const event = "event.test"
notificationService.subscribe(event, "test")
await notificationService.handleEvent(event, {id: "id", return_id: "id"})
expect(container.noti_test.sendNotification).toHaveBeenCalledTimes(1)
})
})
})
@@ -711,6 +711,7 @@ describe("OrderService", () => {
const order = {
fulfillments: [],
shipping_methods: [{ id: "ship" }],
no_notification: true,
items: [
{
id: "item_1",
@@ -859,6 +860,31 @@ describe("OrderService", () => {
fulfillment_status: "partially_fulfilled",
})
})
it.each([
[true, true],
[false, false],
[undefined, true],
])(
"emits correct no_notification option with '%s'",
async (input, expected) => {
await orderService.createFulfillment(
"test-order",
[
{
item_id: "item_1",
quantity: 1,
},
],
{ no_notification: input }
)
expect(eventBusService.emit).toHaveBeenCalledWith(expect.any(String), {
id: expect.any(String),
no_notification: expected,
})
}
)
})
describe("registerReturnReceived", () => {
@@ -975,6 +1001,7 @@ describe("OrderService", () => {
fulfilled_quantity: 0,
},
],
no_notification: true,
}
const orderRepo = MockRepository({
@@ -996,7 +1023,11 @@ describe("OrderService", () => {
}
const fulfillmentService = {
retrieve: () => Promise.resolve({ order_id: IdMap.getId("test") }),
retrieve: () =>
Promise.resolve({
order_id: IdMap.getId("test"),
no_notification: true,
}),
createShipment: jest
.fn()
.mockImplementation((shipmentId, tracking, meta) => {
@@ -1036,10 +1067,12 @@ describe("OrderService", () => {
)
expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1)
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
expect(
fulfillmentService.createShipment
).toHaveBeenCalledWith(
IdMap.getId("fulfillment"),
[{ tracking_number: "1234" }, { tracking_number: "2345" }],
{}
{ metadata: undefined, no_notification: true }
)
expect(orderRepo.save).toHaveBeenCalledTimes(1)
@@ -1048,6 +1081,27 @@ describe("OrderService", () => {
fulfillment_status: "shipped",
})
})
it.each([
[true, true],
[false, false],
[undefined, true],
])(
"emits correct no_notification option with '%s'",
async (input, expected) => {
await orderService.createShipment(
IdMap.getId("test"),
IdMap.getId("fulfillment"),
[{ tracking_number: "1234" }, { tracking_number: "2345" }],
{ no_notification: input }
)
expect(eventBusService.emit).toHaveBeenCalledWith(expect.any(String), {
id: expect.any(String),
no_notification: expected,
})
}
)
})
describe("createRefund", () => {
@@ -1081,6 +1135,7 @@ describe("OrderService", () => {
paid_total: 100,
refundable_amount: 100,
refunded_total: 0,
no_notification: true,
})
},
})
@@ -1129,5 +1184,27 @@ describe("OrderService", () => {
)
).rejects.toThrow("Cannot refund more than the original order amount")
})
it.each([
[false, false],
[undefined, true],
])(
"emits correct no_notification option with '%s'",
async (input, expected) => {
await orderService.createRefund(
IdMap.getId("order_123"),
100,
"discount",
"note",
{ no_notification: input }
)
expect(eventBusService.emit).toHaveBeenCalledWith(expect.any(String), {
id: expect.any(String),
no_notification: expected,
refund_id: expect.any(String),
})
}
)
})
})
@@ -135,8 +135,8 @@ describe("ProductCollectionService", () => {
it("successfully removes a product collection", async () => {
await productCollectionService.delete(IdMap.getId("bathrobe"))
expect(productCollectionRepository.remove).toHaveBeenCalledTimes(1)
expect(productCollectionRepository.remove).toHaveBeenCalledWith({
expect(productCollectionRepository.softRemove).toHaveBeenCalledTimes(1)
expect(productCollectionRepository.softRemove).toHaveBeenCalledWith({
id: IdMap.getId("bathrobe"),
})
})
@@ -57,6 +57,7 @@ const testOrder = generateOrder(
currency_code: "dkk",
region_id: IdMap.getId("region"),
tax_rate: 0,
no_notification: true,
shipping_address: {
first_name: "test",
last_name: "testson",
@@ -329,6 +330,7 @@ describe("SwapService", () => {
order_id: IdMap.getId("test"),
fulfillment_status: "not_fulfilled",
payment_status: "not_paid",
no_notification: true,
additional_items: [
{
unit_price: 100,
@@ -340,6 +342,31 @@ describe("SwapService", () => {
expect(returnService.create).toHaveBeenCalledTimes(1)
})
it.each([
[true, true],
[false, false],
[undefined, true],
])(
"passes correct no_notification to eventBus with %s",
async (input, expected) => {
await swapService.create(
testOrder,
[{ item_id: IdMap.getId("line"), quantity: 1 }],
[{ variant_id: IdMap.getId("new-variant"), quantity: 1 }],
{
id: IdMap.getId("return-shipping"),
price: 20,
},
{ no_notification: input }
)
expect(eventBusService.emit).toHaveBeenCalledWith(
expect.any(String),
{ id: undefined, no_notification: expected }
)
}
)
})
})
+49 -4
View File
@@ -101,7 +101,7 @@ class ClaimService extends BaseService {
const claimRepo = manager.getCustomRepository(this.claimRepository_)
const claim = await this.retrieve(id, { relations: ["shipping_methods"] })
const { claim_items, shipping_methods, metadata } = data
const { claim_items, shipping_methods, metadata, no_notification } = data
if (metadata) {
claim.metadata = this.setMetadata_(claim, metadata)
@@ -135,6 +135,11 @@ class ClaimService extends BaseService {
}
}
if (no_notification !== undefined) {
claim.no_notification = no_notification
await claimRepo.save(claim)
}
if (claim_items) {
for (const i of claim_items) {
if (i.id) {
@@ -149,6 +154,7 @@ class ClaimService extends BaseService {
.withTransaction(manager)
.emit(ClaimService.Events.UPDATED, {
id: claim.id,
no_notification: claim.no_notification,
})
return claim
@@ -174,6 +180,7 @@ class ClaimService extends BaseService {
refund_amount,
shipping_address,
shipping_address_id,
no_notification,
...rest
} = data
@@ -233,6 +240,9 @@ class ClaimService extends BaseService {
)
)
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : order.no_notification
const created = claimRepo.create({
shipping_address_id: addressId,
payment_status: type === "refund" ? "not_refunded" : "na",
@@ -241,6 +251,7 @@ class ClaimService extends BaseService {
type,
additional_items: newItems,
order_id: order.id,
no_notification: evaluatedNoNotification,
})
const result = await claimRepo.save(created)
@@ -281,6 +292,7 @@ class ClaimService extends BaseService {
metadata: ci.metadata,
})),
shipping_method: return_shipping,
no_notification: evaluatedNoNotification,
})
}
@@ -288,13 +300,22 @@ class ClaimService extends BaseService {
.withTransaction(manager)
.emit(ClaimService.Events.CREATED, {
id: result.id,
no_notification: result.no_notification,
})
return result
})
}
createFulfillment(id, metadata = {}) {
createFulfillment(
id,
config = {
metadata: {},
no_notification: undefined,
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
const claim = await this.retrieve(id, {
relations: [
@@ -331,6 +352,9 @@ class ClaimService extends BaseService {
)
}
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : claim.no_notification
const fulfillments = await this.fulfillmentService_
.withTransaction(manager)
.createFulfillment(
@@ -347,6 +371,7 @@ class ClaimService extends BaseService {
items: claim.additional_items,
shipping_methods: claim.shipping_methods,
is_claim: true,
no_notification: evaluatedNoNotification,
},
claim.additional_items.map(i => ({
item_id: i.id,
@@ -395,6 +420,7 @@ class ClaimService extends BaseService {
.emit(ClaimService.Events.FULFILLMENT_CREATED, {
id: id,
fulfillment_id: fulfillment.id,
no_notification: claim.no_notification,
})
}
@@ -430,21 +456,38 @@ class ClaimService extends BaseService {
.withTransaction(manager)
.emit(ClaimService.Events.REFUND_PROCESSED, {
id,
no_notification: result.no_notification,
})
return result
})
}
async createShipment(id, fulfillmentId, trackingLinks, metadata = []) {
async createShipment(
id,
fulfillmentId,
trackingLinks,
config = {
metadata: {},
no_notification: undefined,
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
const claim = await this.retrieve(id, {
relations: ["additional_items"],
})
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : claim.no_notification
const shipment = await this.fulfillmentService_
.withTransaction(manager)
.createShipment(fulfillmentId, trackingLinks, metadata)
.createShipment(fulfillmentId, trackingLinks, {
metadata,
no_notification: evaluatedNoNotification,
})
claim.fulfillment_status = "shipped"
@@ -474,6 +517,7 @@ class ClaimService extends BaseService {
.emit(ClaimService.Events.SHIPMENT_CREATED, {
id,
fulfillment_id: shipment.id,
no_notification: evaluatedNoNotification,
})
return result
@@ -524,6 +568,7 @@ class ClaimService extends BaseService {
.withTransaction(manager)
.emit(ClaimService.Events.CANCELED, {
id: result.id,
no_notification: result.no_notification,
})
return result
+48 -2
View File
@@ -318,6 +318,13 @@ class CustomerService extends BaseService {
const existing = await this.retrieveByEmail(email).catch(err => undefined)
if (existing && existing.has_account) {
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
"A customer with the given email already has an account. Log in instead"
)
}
if (existing && password && !existing.has_account) {
const hashedPassword = await this.hashPassword_(password)
customer.password_hash = hashedPassword
@@ -360,6 +367,7 @@ class CustomerService extends BaseService {
const customerRepository = manager.getCustomRepository(
this.customerRepository_
)
const addrRepo = manager.getCustomRepository(this.addressRepository_)
const customer = await this.retrieve(customerId)
@@ -368,6 +376,7 @@ class CustomerService extends BaseService {
password,
password_hash,
billing_address,
billing_address_id,
metadata,
...rest
} = update
@@ -380,8 +389,9 @@ class CustomerService extends BaseService {
customer.email = this.validateEmail_(email)
}
if (billing_address) {
customer.billing_address = this.validateBillingAddress_(billing_address)
if ("billing_address_id" in update || "billing_address" in update) {
const address = update.billing_address_id || update.billing_address
await this.updateBillingAddress_(customer, address, addrRepo)
}
for (const [key, value] of Object.entries(rest)) {
@@ -393,6 +403,7 @@ class CustomerService extends BaseService {
}
const updated = await customerRepository.save(customer)
await this.eventBus_
.withTransaction(manager)
.emit(CustomerService.Events.UPDATED, updated)
@@ -400,6 +411,41 @@ class CustomerService extends BaseService {
})
}
/**
* Updates the customers's billing address.
* @param {Customer} customer - the Customer to update
* @param {object} address - the value to set the billing address to
* @return {Promise} the result of the update operation
*/
async updateBillingAddress_(customer, addressOrId, addrRepo) {
if (typeof addressOrId === `string`) {
addressOrId = await addrRepo.findOne({
where: { id: addressOrId },
})
}
addressOrId.country_code = addressOrId.country_code.toLowerCase()
if (addressOrId.id) {
customer.billing_address_id = addressOrId.id
customer.billing_address = addressOrId
} else {
if (customer.billing_address_id) {
const addr = await addrRepo.findOne({
where: { id: customer.billing_address_id },
})
await addrRepo.save({ ...addr, ...addressOrId })
} else {
const created = addrRepo.create({
...addressOrId,
})
const saved = await addrRepo.save(created)
customer.billing_address = saved
}
}
}
async updateAddress(customerId, addressId, address) {
return this.atomicPhase_(async manager => {
const addressRepo = manager.getCustomRepository(this.addressRepository_)
+50 -2
View File
@@ -10,6 +10,7 @@ import { Brackets } from "typeorm"
class DraftOrderService extends BaseService {
static Events = {
CREATED: "draft_order.created",
UPDATED: "draft_order.updated",
}
constructor({
@@ -248,7 +249,13 @@ class DraftOrderService extends BaseService {
)
}
const { items, shipping_methods, discounts, ...rest } = data
const {
items,
shipping_methods,
discounts,
no_notification_order,
...rest
} = data
if (discounts) {
for (const { code } of discounts) {
@@ -263,7 +270,10 @@ class DraftOrderService extends BaseService {
.withTransaction(manager)
.create({ type: "draft_order", ...rest })
const draftOrder = draftOrderRepo.create({ cart_id: createdCart.id })
const draftOrder = draftOrderRepo.create({
cart_id: createdCart.id,
no_notification_order,
})
const result = await draftOrderRepo.save(draftOrder)
await this.eventBus_
@@ -335,6 +345,44 @@ class DraftOrderService extends BaseService {
await draftOrderRepo.save(draftOrder)
})
}
/**
* Updates a draft order with the given data
* @param {String} doId - id of the draft order
* @param {DraftOrder} data - values to update the order with
* @returns {Promise<DraftOrder>} the updated draft order
*/
async update(doId, data) {
return this.atomicPhase_(async manager => {
const doRepo = manager.getCustomRepository(this.draftOrderRepository_)
const draftOrder = await this.retrieve(doId)
let touched = false
if (draftOrder.status === "completed") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Can't update a draft order which is complete"
)
}
if (data.no_notification_order !== undefined) {
touched = true
draftOrder.no_notification_order = data.no_notification_order
}
if (touched) {
doRepo.save(draftOrder)
await this.eventBus_
.withTransaction(manager)
.emit(DraftOrderService.Events.UPDATED, {
id: draftOrder.id,
})
}
return draftOrder
})
}
}
export default DraftOrderService
+15 -2
View File
@@ -241,10 +241,19 @@ class FulfillmentService extends BaseService {
* tracking numbers and potentially more metadata.
* @param {Order} fulfillmentId - the fulfillment to ship
* @param {TrackingLink[]} trackingNumbers - tracking numbers for the shipment
* @param {object} metadata - potential metadata to add
* @param {object} config - potential configuration settings, such as no_notification and metadata
* @return {Fulfillment} the shipped fulfillment
*/
async createShipment(fulfillmentId, trackingLinks, metadata) {
async createShipment(
fulfillmentId,
trackingLinks,
config = {
metadata: {},
no_notification: undefined,
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_
@@ -264,6 +273,10 @@ class FulfillmentService extends BaseService {
trackingLinkRepo.create(tl)
)
if (no_notification) {
fulfillment.no_notification = no_notification
}
fulfillment.metadata = {
...fulfillment.metadata,
...metadata,
+56 -5
View File
@@ -1,7 +1,8 @@
import _ from "lodash"
import randomize from "randomatic"
import { BaseService } from "medusa-interfaces"
import { Validator, MedusaError } from "medusa-core-utils"
import { Brackets } from "typeorm"
import { MedusaError } from "medusa-core-utils"
/**
* Provides layer to manipulate gift cards.
@@ -72,6 +73,7 @@ class GiftCardService extends BaseService {
/**
* @param {Object} selector - the query object for find
* @param {Object} config - the configuration used to find the objects. contains relations, skip, and take.
* @return {Promise} the result of the find operation
*/
async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) {
@@ -79,8 +81,41 @@ class GiftCardService extends BaseService {
this.giftCardRepository_
)
let q
if ("q" in selector) {
q = selector.q
delete selector.q
}
const query = this.buildQuery_(selector, config)
return giftCardRepo.find(query)
const rels = query.relations
delete query.relations
if (q) {
const where = query.where
delete where.id
const raw = await giftCardRepo
.createQueryBuilder("gift_card")
.leftJoinAndSelect("gift_card.order", "order")
.select(["gift_card.id"])
.where(where)
.andWhere(
new Brackets(qb => {
return qb
.where(`gift_card.code ILIKE :q`, { q: `%${q}%` })
.orWhere(`display_id::varchar(255) ILIKE :dId`, { dId: `${q}` })
})
)
.getMany()
return giftCardRepo.findWithRelations(
rels,
raw.map(i => i.id)
)
}
return giftCardRepo.findWithRelations(rels, query)
}
async createTransaction(data) {
@@ -156,7 +191,10 @@ class GiftCardService extends BaseService {
query.relations = config.relations
}
const giftCard = await giftCardRepo.findOne(query)
const rels = query.relations
delete query.relations
const giftCard = await giftCardRepo.findOneWithRelations(rels, query)
if (!giftCard) {
throw new MedusaError(
@@ -185,7 +223,10 @@ class GiftCardService extends BaseService {
query.relations = config.relations
}
const giftCard = await giftCardRepo.findOne(query)
const rels = query.relations
delete query.relations
const giftCard = await giftCardRepo.findOneWithRelations(rels, query)
if (!giftCard) {
throw new MedusaError(
@@ -209,7 +250,7 @@ class GiftCardService extends BaseService {
const giftCard = await this.retrieve(giftCardId)
const { region_id, metadata, ...rest } = update
const { region_id, metadata, balance, ...rest } = update
if (region_id && region_id !== giftCard.region_id) {
const region = await this.regionService_.retrieve(region_id)
@@ -220,6 +261,16 @@ class GiftCardService extends BaseService {
giftCard.metadata = await this.setMetadata_(giftCard.id, metadata)
}
if (balance) {
if (balance < 0 || giftCard.value < balance) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"new balance is invalid"
)
}
giftCard.balance = balance
}
for (const [key, value] of Object.entries(rest)) {
giftCard[key] = value
}
@@ -164,6 +164,9 @@ class NotificationService extends BaseService {
if (!subs) {
return Promise.resolve()
}
if (data["no_notification"] === true) {
return
}
return Promise.all(
subs.map(async providerId => {
+70 -5
View File
@@ -333,7 +333,6 @@ class OrderService extends BaseService {
const rels = query.relations
delete query.relations
const raw = await orderRepo.findOneWithRelations(rels, query)
if (!raw) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
@@ -408,6 +407,7 @@ class OrderService extends BaseService {
OrderService.Events.COMPLETED,
{
id: orderId,
no_notification: order.no_notification,
}
)
@@ -507,6 +507,7 @@ class OrderService extends BaseService {
.retrieveByCartId(cart.id)
toCreate.draft_order_id = draft.id
toCreate.no_notification = draft.no_notification_order
}
const o = await orderRepo.create(toCreate)
@@ -553,6 +554,7 @@ class OrderService extends BaseService {
.withTransaction(manager)
.emit(OrderService.Events.PLACED, {
id: result.id,
no_notification: result.no_notification,
})
return result
@@ -571,7 +573,17 @@ class OrderService extends BaseService {
* the fulfillment
* @return {order} the resulting order following the update.
*/
async createShipment(orderId, fulfillmentId, trackingLinks, metadata = {}) {
async createShipment(
orderId,
fulfillmentId,
trackingLinks,
config = {
metadata: {},
no_notification: undefined,
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
const order = await this.retrieve(orderId, { relations: ["items"] })
const shipment = await this.fulfillmentService_.retrieve(fulfillmentId)
@@ -583,9 +595,17 @@ class OrderService extends BaseService {
)
}
const evaluatedNoNotification =
no_notification !== undefined
? no_notification
: shipment.no_notification
const shipmentRes = await this.fulfillmentService_
.withTransaction(manager)
.createShipment(fulfillmentId, trackingLinks, metadata)
.createShipment(fulfillmentId, trackingLinks, {
metadata,
no_notification: evaluatedNoNotification,
})
order.fulfillment_status = "shipped"
for (const item of order.items) {
@@ -614,6 +634,7 @@ class OrderService extends BaseService {
.emit(OrderService.Events.SHIPMENT_CREATED, {
id: orderId,
fulfillment_id: shipmentRes.id,
no_notification: evaluatedNoNotification,
})
return result
@@ -634,6 +655,7 @@ class OrderService extends BaseService {
.withTransaction(manager)
.emit(OrderService.Events.PLACED, {
id: result.id,
no_notification: order.no_notification,
})
return result
})
@@ -801,6 +823,10 @@ class OrderService extends BaseService {
await this.updateBillingAddress_(order, update.billing_address)
}
if ("no_notification" in update) {
order.no_notification = update.no_notification
}
if ("items" in update) {
for (const item of update.items) {
await this.lineItemService_.withTransaction(manager).create({
@@ -821,6 +847,7 @@ class OrderService extends BaseService {
.withTransaction(manager)
.emit(OrderService.Events.UPDATED, {
id: orderId,
no_notification: order.no_notification,
})
return result
})
@@ -871,6 +898,7 @@ class OrderService extends BaseService {
.withTransaction(manager)
.emit(OrderService.Events.CANCELED, {
id: order.id,
no_notification: order.no_notification,
})
return result
})
@@ -899,6 +927,7 @@ class OrderService extends BaseService {
id: orderId,
payment_id: p.id,
error: err,
no_notification: order.no_notification,
})
})
@@ -924,6 +953,7 @@ class OrderService extends BaseService {
.withTransaction(manager)
.emit(OrderService.Events.PAYMENT_CAPTURED, {
id: result.id,
no_notification: order.no_notification,
})
}
@@ -970,8 +1000,21 @@ class OrderService extends BaseService {
* @param {string} orderId - id of order to cancel.
* @return {Promise} result of the update operation.
*/
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
async createFulfillment(
orderId,
itemsToFulfill,
config = {
no_notification: undefined,
metadata: {},
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
// NOTE: we are telling the service to calculate all totals for us which
// will add to what is fetched from the database. We want this to happen
// so that we get all order details. These will thereafter be forwarded
// to the fulfillment provider.
const order = await this.retrieve(orderId, {
select: [
"subtotal",
@@ -1007,6 +1050,7 @@ class OrderService extends BaseService {
.withTransaction(manager)
.createFulfillment(order, itemsToFulfill, {
metadata,
no_notification: no_notification,
order_id: orderId,
})
let successfullyFulfilled = []
@@ -1046,12 +1090,16 @@ class OrderService extends BaseService {
order.fulfillments = [...order.fulfillments, ...fulfillments]
const result = await orderRepo.save(order)
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : order.no_notification
for (const fulfillment of fulfillments) {
await this.eventBus_
.withTransaction(manager)
.emit(OrderService.Events.FULFILLMENT_CREATED, {
id: orderId,
fulfillment_id: fulfillment.id,
no_notification: evaluatedNoNotification,
})
}
@@ -1107,7 +1155,17 @@ class OrderService extends BaseService {
/**
* Refunds a given amount back to the customer.
*/
async createRefund(orderId, refundAmount, reason, note) {
async createRefund(
orderId,
refundAmount,
reason,
note,
config = {
no_notification: undefined,
}
) {
const { no_notification } = config
return this.atomicPhase_(async manager => {
const order = await this.retrieve(orderId, {
select: ["refundable_amount", "total", "refunded_total"],
@@ -1126,9 +1184,14 @@ class OrderService extends BaseService {
.refundPayment(order.payments, refundAmount, reason, note)
const result = await this.retrieve(orderId)
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : order.no_notification
this.eventBus_.emit(OrderService.Events.REFUND_CREATED, {
id: result.id,
refund_id: refund.id,
no_notification: evaluatedNoNotification,
})
return result
})
@@ -1232,6 +1295,7 @@ class OrderService extends BaseService {
.emit(OrderService.Events.RETURN_ACTION_REQUIRED, {
id: result.id,
return_id: receivedReturn.id,
no_notification: receivedReturn.no_notification,
})
return result
}
@@ -1263,6 +1327,7 @@ class OrderService extends BaseService {
.emit(OrderService.Events.ITEMS_RETURNED, {
id: order.id,
return_id: receivedReturn.id,
no_notification: receivedReturn.no_notification,
})
return result
})
@@ -129,7 +129,7 @@ class ProductCollectionService extends BaseService {
if (!collection) return Promise.resolve()
await productCollectionRepo.remove(collection)
await productCollectionRepo.softRemove(collection)
return Promise.resolve()
})
+1
View File
@@ -330,6 +330,7 @@ class ReturnService extends BaseService {
reason_id: i.reason_id,
note: i.note,
metadata: i.metadata,
no_notification: data.no_notification,
})
)
+95 -12
View File
@@ -16,6 +16,7 @@ class SwapService extends BaseService {
PAYMENT_CAPTURE_FAILED: "swap.payment_capture_failed",
PROCESS_REFUND_FAILED: "swap.process_refund_failed",
REFUND_PROCESSED: "swap.refund_processed",
FULFILLMENT_CREATED: "swap.fulfillment_created",
}
constructor({
@@ -210,6 +211,9 @@ class SwapService extends BaseService {
* the customer.
* @param {ReturnShipping?} returnShipping - an optional shipping method for
* returning the returnItems.
* @param {Object} custom - contains relevant custom information. This object may
* include no_notification which will disable sending notification when creating
* swap. If set, it overrules the attribute inherited from the order.
* @returns {Promise<Swap>} the newly created swap.
*/
async create(
@@ -217,8 +221,11 @@ class SwapService extends BaseService {
returnItems,
additionalItems,
returnShipping,
custom = {}
custom = {
no_notification: undefined,
}
) {
const { no_notification, ...rest } = custom
return this.atomicPhase_(async manager => {
if (
order.fulfillment_status === "not_fulfilled" ||
@@ -240,13 +247,17 @@ class SwapService extends BaseService {
})
)
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : order.no_notification
const swapRepo = manager.getCustomRepository(this.swapRepository_)
const created = swapRepo.create({
...custom,
...rest,
fulfillment_status: "not_fulfilled",
payment_status: "not_paid",
order_id: order.id,
additional_items: newItems,
no_notification: evaluatedNoNotification,
})
const result = await swapRepo.save(created)
@@ -256,12 +267,14 @@ class SwapService extends BaseService {
order_id: order.id,
items: returnItems,
shipping_method: returnShipping,
no_notification: evaluatedNoNotification,
})
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.CREATED, {
id: result.id,
no_notification: evaluatedNoNotification,
})
return result
@@ -298,9 +311,14 @@ class SwapService extends BaseService {
} catch (err) {
swap.payment_status = "requires_action"
const result = await swapRepo.save(swap)
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.PROCESS_REFUND_FAILED, result)
.emit(SwapService.Events.PROCESS_REFUND_FAILED, {
id: result.id,
no_notification: swap.no_notification,
})
return result
}
@@ -310,7 +328,11 @@ class SwapService extends BaseService {
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.REFUND_PROCESSED, result)
.emit(SwapService.Events.REFUND_PROCESSED, {
id: result.id,
no_notification: swap.no_notification,
})
return result
} else if (swap.difference_due === 0) {
if (swap.payment_status === "difference_refunded") {
@@ -320,9 +342,14 @@ class SwapService extends BaseService {
swap.payment_status = "difference_refunded"
const result = await swapRepo.save(swap)
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.REFUND_PROCESSED, result)
.emit(SwapService.Events.REFUND_PROCESSED, {
id: result.id,
no_notification: swap.no_notification,
})
return result
}
@@ -337,18 +364,28 @@ class SwapService extends BaseService {
} catch (err) {
swap.payment_status = "requires_action"
const result = await swapRepo.save(swap)
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.PAYMENT_CAPTURE_FAILED, result)
.emit(SwapService.Events.PAYMENT_CAPTURE_FAILED, {
id: swap.id,
no_notification: swap.no_notification,
})
return result
}
swap.payment_status = "captured"
const result = await swapRepo.save(swap)
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.PAYMENT_CAPTURED, result)
.emit(SwapService.Events.PAYMENT_CAPTURED, {
id: result.id,
no_notification: swap.no_notification,
})
return result
})
}
@@ -361,6 +398,10 @@ class SwapService extends BaseService {
swap.metadata = this.setMetadata_(swap, update.metadata)
}
if ("no_notification" in update) {
swap.no_notification = update.no_notification
}
if ("shipping_address" in update) {
await this.updateShippingAddress_(swap, update.shipping_address)
}
@@ -562,6 +603,7 @@ class SwapService extends BaseService {
.withTransaction(manager)
.emit(SwapService.Events.PAYMENT_COMPLETED, {
id: swap.id,
no_notification: swap.no_notification,
})
return result
@@ -609,10 +651,18 @@ class SwapService extends BaseService {
* Fulfills the addtional items associated with the swap. Will call the
* fulfillment providers associated with the shipping methods.
* @param {string} swapId - the id of the swap to fulfill,
* @param {object} metadata - optional metadata to attach to the fulfillment.
* @param {object} config - optional configurations, includes optional metadata to attach to the shipment, and a no_notification flag.
* @returns {Promise<Swap>} the updated swap with new status and fulfillments.
*/
async createFulfillment(swapId, metadata = {}) {
async createFulfillment(
swapId,
config = {
metadata: {},
no_notification: undefined,
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
const swap = await this.retrieve(swapId, {
relations: [
@@ -642,6 +692,9 @@ class SwapService extends BaseService {
)
}
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : swap.no_notification
swap.fulfillments = await this.fulfillmentService_
.withTransaction(manager)
.createFulfillment(
@@ -658,6 +711,7 @@ class SwapService extends BaseService {
items: swap.additional_items,
shipping_methods: swap.shipping_methods,
is_swap: true,
no_notification: evaluatedNoNotification,
},
swap.additional_items.map(i => ({
item_id: i.id,
@@ -700,6 +754,17 @@ class SwapService extends BaseService {
const swapRepo = manager.getCustomRepository(this.swapRepository_)
const result = await swapRepo.save(swap)
await this.eventBus_.withTransaction(manager).emit(
SwapService.Events.FULFILLMENT_CREATED,
{
id: swapId,
fulfillment_id: result.id,
no_notification: evaluatedNoNotification,
}
)
return result
})
}
@@ -711,19 +776,35 @@ class SwapService extends BaseService {
* has been shipped
* @param {TrackingLink[]} trackingLinks - the tracking numbers associated
* with the shipment
* @param {object} metadata - optional metadata to attach to the shipment.
* @param {object} config - optional configurations, includes optional metadata to attach to the shipment, and a noNotification flag.
* @returns {Promise<Swap>} the updated swap with new fulfillments and status.
*/
async createShipment(swapId, fulfillmentId, trackingLinks, metadata = {}) {
async createShipment(
swapId,
fulfillmentId,
trackingLinks,
config = {
metadata: {},
no_notification: undefined,
}
) {
const { metadata, no_notification } = config
return this.atomicPhase_(async manager => {
const swap = await this.retrieve(swapId, {
relations: ["additional_items"],
})
const evaluatedNoNotification =
no_notification !== undefined ? no_notification : swap.no_notification
// Update the fulfillment to register
const shipment = await this.fulfillmentService_
.withTransaction(manager)
.createShipment(fulfillmentId, trackingLinks, metadata)
.createShipment(fulfillmentId, trackingLinks, {
metadata,
no_notification: evaluatedNoNotification,
})
swap.fulfillment_status = "shipped"
@@ -753,6 +834,7 @@ class SwapService extends BaseService {
.emit(SwapService.Events.SHIPMENT_CREATED, {
id: swapId,
fulfillment_id: shipment.id,
no_notification: swap.no_notification,
})
return result
})
@@ -809,6 +891,7 @@ class SwapService extends BaseService {
.emit(SwapService.Events.RECEIVED, {
id: id,
order_id: result.order_id,
no_notification: swap.no_notification,
})
return result