feat(medusa): Migrate and fix order service (#1894)

* feat(medusa): Migrate and fix order service

* fix(medusa): Order service pass Payment instead of the session:

* fix(medusa): Remove unnecessary method in the client library

* test(medusa): Fix unit tests

* test(medusa): Fix unit tests

* fix(medusa): Typo

* test(meduas): fix unit test

* feat(medusa): Update base service used and missing transaction

* test(meduas): fix unit test

* fix(medusa): cleanup and missing transaction

* fix(medusa): missing withTransaction on some mocks

* feat(medusa): Update order service method visibility

* include feedback

* feat(medusa); revert order payment status"

* test(medusa): fix unit

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de PerettiandOliver Windall Juhl authored and GitHub committed 2022-08-01 11:01:17 +02:00
1 parent 1998902a94
commit b54d5178c9
34 files changed
+531 -1029

No files matched your search

@@ -15,21 +15,12 @@ import {
AdminPostOrdersOrderSwapsReq, AdminPostOrdersOrderSwapsReq,
AdminPostOrdersOrderSwapsSwapFulfillmentsReq, AdminPostOrdersOrderSwapsSwapFulfillmentsReq,
AdminPostOrdersOrderSwapsSwapShipmentsReq, AdminPostOrdersOrderSwapsSwapShipmentsReq,
AdminPostOrdersReq,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import qs from "qs" import qs from "qs"
import { ResponsePromise } from "../../typings" import { ResponsePromise } from "../../typings"
import BaseResource from "../base" import BaseResource from "../base"
class AdminOrdersResource extends BaseResource { class AdminOrdersResource extends BaseResource {
create(
payload: AdminPostOrdersReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminOrdersRes> {
const path = `/admin/orders`
return this.client.request("POST", path, payload, {}, customHeaders)
}
update( update(
id: string, id: string,
payload: AdminPostOrdersOrderReq, payload: AdminPostOrdersOrderReq,
@@ -261,15 +252,6 @@ class AdminOrdersResource extends BaseResource {
const path = `/admin/orders/${id}/claims/${claimId}/shipments` const path = `/admin/orders/${id}/claims/${claimId}/shipments`
return this.client.request("POST", path, payload, {}, customHeaders) return this.client.request("POST", path, payload, {}, customHeaders)
} }
deleteMetadata(
id: string,
key: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminOrdersRes> {
const path = `/admin/orders/${id}/metadata/${key}`
return this.client.request("DELETE", path, undefined, {}, customHeaders)
}
} }
export default AdminOrdersResource export default AdminOrdersResource
@@ -7,28 +7,12 @@ import {
AdminPostOrdersOrderReturnsReq, AdminPostOrdersOrderReturnsReq,
AdminPostOrdersOrderShipmentReq, AdminPostOrdersOrderShipmentReq,
AdminPostOrdersOrderShippingMethodsReq, AdminPostOrdersOrderShippingMethodsReq,
AdminPostOrdersReq,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js" import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query" import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { useMedusa } from "../../../contexts/medusa" import { useMedusa } from "../../../contexts/medusa"
import { buildOptions } from "../../utils/buildOptions" import { buildOptions } from "../../utils/buildOptions"
export const useAdminCreateOrder = (
options?: UseMutationOptions<
Response<AdminOrdersRes>,
Error,
AdminPostOrdersReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostOrdersReq) => client.admin.orders.create(payload),
buildOptions(queryClient, adminOrderKeys.lists(), options)
)
}
export const useAdminUpdateOrder = ( export const useAdminUpdateOrder = (
id: string, id: string,
options?: UseMutationOptions< options?: UseMutationOptions<
@@ -234,16 +218,3 @@ export const useAdminArchiveOrder = (
) )
) )
} }
export const useAdminDeleteOrderMetadata = (
id: string,
options?: UseMutationOptions<Response<AdminOrdersRes>, Error, string>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(key: string) => client.admin.orders.deleteMetadata(id, key),
buildOptions(queryClient, adminOrderKeys.detail(id), options)
)
}
@@ -1,7 +1,5 @@
import { import {
useAdminCreateOrder,
useAdminUpdateOrder, useAdminUpdateOrder,
useAdminDeleteOrderMetadata,
useAdminCompleteOrder, useAdminCompleteOrder,
useAdminCapturePayment, useAdminCapturePayment,
useAdminRefundPayment, useAdminRefundPayment,
@@ -16,67 +14,6 @@ import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data" import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils" import { createWrapper } from "../../../utils"
describe("useAdminCreateOrder hook", () => {
test("creates a order and returns it", async () => {
const order = {
email: "lebron@james.com",
billing_address: {
company: "medusa",
first_name: "Jane",
last_name: "Medusan",
address_1: "jane street",
address_2: "2nd floor",
city: "copenhagen",
country_code: "dk",
province: "copenhagen",
postal_code: "382793",
phone: "4897394",
metadata: null,
},
shipping_address: {
company: "medusa",
first_name: "Jane",
last_name: "Medusan",
address_1: "jane street",
address_2: "2nd floor",
city: "copenhagen",
country_code: "dk",
province: "copenhagen",
postal_code: "382793",
phone: "4897394",
metadata: null,
},
items: [
{
variant_id: "test-variant",
quantity: 1,
},
],
region: "test-region",
customer_id: "cus_test",
payment_method: {
provider_id: "test-pay",
},
}
const { result, waitFor } = renderHook(() => useAdminCreateOrder(), {
wrapper: createWrapper(),
})
result.current.mutate(order)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.order).toEqual(
expect.objectContaining({
...fixtures.get("order"),
...order,
})
)
})
})
describe("useAdminUpdateOrder hook", () => { describe("useAdminUpdateOrder hook", () => {
test("updates a order and returns it", async () => { test("updates a order and returns it", async () => {
const order = { const order = {
@@ -290,23 +227,3 @@ describe("useAdminCancelFulfillment hook", () => {
expect(result.current.data.order).toEqual(fixtures.get("order")) expect(result.current.data.order).toEqual(fixtures.get("order"))
}) })
}) })
describe("useAdminDeleteOrderMetadata hook", () => {
test("remove metadata field on order", async () => {
const id = fixtures.get("order").id
const { result, waitFor } = renderHook(
() => useAdminDeleteOrderMetadata(id),
{
wrapper: createWrapper(),
}
)
result.current.mutate("some_key")
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.order).toEqual(fixtures.get("order"))
})
})
@@ -1,144 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import {
orders,
OrderServiceMock,
} from "../../../../../services/__mocks__/order"
describe("POST /admin/orders", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/orders", {
payload: {
email: "virgil@vandijk.dk",
billing_address: {
first_name: "Virgil",
last_name: "Van Dijk",
address_1: "24 Dunks Drive",
city: "Los Angeles",
country_code: "US",
province: "CA",
postal_code: "93011",
phone: "+1 (222) 333 4444",
},
shipping_address: {
first_name: "Virgil",
last_name: "Van Dijk",
address_1: "24 Dunks Drive",
city: "Los Angeles",
country_code: "US",
province: "CA",
postal_code: "93011",
phone: "+1 (222) 333 4444",
},
items: [
{
_id: IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("validId"),
},
quantity: 1,
},
quantity: 10,
},
],
region: IdMap.getId("testRegion"),
customer_id: IdMap.getId("testCustomer"),
payment_method: {
provider_id: "default_provider",
data: {},
},
shipping_method: [
{
provider_id: "default_provider",
profile_id: IdMap.getId("validId"),
price: 123,
data: {},
items: [],
},
],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls OrderService create", () => {
expect(OrderServiceMock.create).toHaveBeenCalledTimes(1)
expect(OrderServiceMock.create).toHaveBeenCalledWith({
email: "virgil@vandijk.dk",
billing_address: {
first_name: "Virgil",
last_name: "Van Dijk",
address_1: "24 Dunks Drive",
city: "Los Angeles",
country_code: "US",
province: "CA",
postal_code: "93011",
phone: "+1 (222) 333 4444",
},
shipping_address: {
first_name: "Virgil",
last_name: "Van Dijk",
address_1: "24 Dunks Drive",
city: "Los Angeles",
country_code: "US",
province: "CA",
postal_code: "93011",
phone: "+1 (222) 333 4444",
},
items: [
{
_id: IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("validId"),
},
quantity: 1,
},
quantity: 10,
},
],
region: IdMap.getId("testRegion"),
customer_id: IdMap.getId("testCustomer"),
payment_method: {
provider_id: "default_provider",
data: {},
},
shipping_method: [
{
provider_id: "default_provider",
profile_id: IdMap.getId("validId"),
price: 123,
data: {},
items: [],
},
],
})
})
})
})
@@ -1,35 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
describe("DELETE /admin/orders/:id/metadata/key", () => {
describe("successfully deletes metadata on order", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/orders/${IdMap.getId("test-order")}/metadata/test-key`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls OrderService deleteMetadata", () => {
expect(OrderServiceMock.deleteMetadata).toHaveBeenCalledTimes(1)
expect(OrderServiceMock.deleteMetadata).toHaveBeenCalledWith(
IdMap.getId("test-order"),
"test-key"
)
})
})
})
@@ -92,7 +92,7 @@ export class AdminPostOrdersOrderFulfillmentsReq {
@IsObject() @IsObject()
@IsOptional() @IsOptional()
metadata?: object metadata?: Record<string, unknown>
} }
class Item { class Item {
@@ -1,203 +0,0 @@
import { Type } from "class-transformer"
import {
IsEmail,
IsOptional,
ValidateNested,
IsArray,
IsString,
IsBoolean,
IsObject,
IsInt,
IsNotEmpty,
} from "class-validator"
import { OrderService } from "../../../../services"
import { AddressPayload } from "../../../../types/common"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /orders
* operationId: "PostOrders"
* summary: "Create an order"
* description: "Creates and order"
* x-authenticated: true
* requestBody:
* content:
* application/json:
* required:
* - email
* - billing_address
* - shipping_address
* - items
* - region
* - customer_id
* - payment_method
* - shipping_method
* schema:
* properties:
* status:
* description: status of the order
* type: string
* email:
* description: the email for the order
* type: string
* billing_address:
* description: Billing address
* anyOf:
* - $ref: "#/components/schemas/address
* shipping_address:
* description: Shipping address
* anyOf:
* - $ref: "#/components/schemas/address
* items:
* description: The Line Items for the order
* type: array
* region:
* description: Region where the order belongs
* type: string
* discounts:
* description: Discounts applied to the order
* type: array
* customer_id:
* description: id of the customer
* type: string
* payment_method:
* description: payment method chosen for the order
* type: object
* required:
* - provider_id
* properties:
* provider_id:
* type: string
* description: id of the payment provider
* data:
* description: Data relevant for the given payment method
* type: object
* shipping_method:
* description: The Shipping Method used for shipping the order.
* type: object
* required:
* - provider_id
* - profile_id
* - price
* properties:
* provider_id:
* type: string
* description: The id of the shipping provider.
* profile_id:
* type: string
* description: The id of the shipping profile.
* price:
* type: integer
* description: The price of the shipping.
* data:
* type: object
* description: Data relevant to the specific shipping method.
* items:
* type: array
* description: Items to ship
* no_notification:
* description: A flag to indicate if no notifications should be emitted related to the updated order.
* type: boolean
* tags:
* - Order
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* order:
* $ref: "#/components/schemas/order"
*/
export default async (req, res) => {
const validated = await validator(AdminPostOrdersReq, req.body)
const orderService: OrderService = req.scope.resolve("orderService")
let order = await orderService.create(validated)
order = await orderService.decorate(order, [], ["region"])
res.status(200).json({ order })
}
export class AdminPostOrdersReq {
@IsString()
@IsOptional()
status?: string
@IsEmail()
@IsNotEmpty()
email: string
@IsNotEmpty()
@ValidateNested()
@Type(() => AddressPayload)
billing_address: AddressPayload
@IsNotEmpty()
@ValidateNested()
@Type(() => AddressPayload)
shipping_address: AddressPayload
@IsArray()
@IsNotEmpty()
items: object[]
@IsString()
@IsNotEmpty()
region: string
@IsArray()
@IsOptional()
discounts?: object[]
@IsString()
@IsNotEmpty()
customer_id: string
@IsNotEmpty()
@ValidateNested()
@Type(() => PaymentMethod)
payment_method: PaymentMethod
@IsNotEmpty()
@ValidateNested({ each: true })
@Type(() => ShippingMethod)
shipping_method?: ShippingMethod[]
@IsBoolean()
@IsOptional()
no_notification?: boolean
}
class PaymentMethod {
@IsString()
@IsNotEmpty()
provider_id: string
@IsObject()
@IsOptional()
data?: object
}
class ShippingMethod {
@IsString()
@IsNotEmpty()
provider_id: string
@IsString()
@IsNotEmpty()
profile_id: string
@IsInt()
@IsNotEmpty()
price: number
@IsObject()
@IsOptional()
data?: object
@IsArray()
@IsOptional()
items?: object[]
}
@@ -8,6 +8,7 @@ import {
import { defaultAdminOrdersRelations, defaultAdminOrdersFields } from "." import { defaultAdminOrdersRelations, defaultAdminOrdersFields } from "."
import { OrderService } from "../../../../services" import { OrderService } from "../../../../services"
import { validator } from "../../../../utils/validator" import { validator } from "../../../../utils/validator"
import { TrackingLink } from "../../../../models"
/** /**
* @oas [post] /orders/{id}/shipment * @oas [post] /orders/{id}/shipment
* operationId: "PostOrdersOrderShipment" * operationId: "PostOrdersOrderShipment"
@@ -56,8 +57,13 @@ export default async (req, res) => {
await orderService.createShipment( await orderService.createShipment(
id, id,
validated.fulfillment_id, validated.fulfillment_id,
validated.tracking_numbers?.map((n) => ({ tracking_number: n })), validated.tracking_numbers?.map((n) => ({
{ no_notification: validated.no_notification } tracking_number: n,
})) as TrackingLink[],
{
metadata: {},
no_notification: validated.no_notification,
}
) )
const order = await orderService.retrieve(id, { const order = await orderService.retrieve(id, {
@@ -1,36 +0,0 @@
import { OrderService } from "../../../../services"
/**
* @oas [delete] /order/{id}/metadata/{key}
* operationId: "DeleteOrdersOrderMetadataKey"
* summary: "Delete Metadata"
* description: "Deletes a metadata key."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The id of the Order.
* - (path) key=* {string} The metadata key.
* tags:
* - Order
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* order:
* $ref: "#/components/schemas/order"
*/
export default async (req, res) => {
const { id, key } = req.params
const orderService: OrderService = req.scope.resolve("orderService")
await orderService.deleteMetadata(id, key)
const order = await orderService.retrieve(id, {
relations: ["region", "customer", "swaps"],
})
res.status(200).json({ order })
}
@@ -48,11 +48,6 @@ export default (app, featureFlagRouter: FlagRouter) => {
middlewares.wrap(require("./get-order").default) middlewares.wrap(require("./get-order").default)
) )
/**
* Create a new order
*/
route.post("/", middlewares.wrap(require("./create-order").default))
/** /**
* Update an order * Update an order
*/ */
@@ -225,14 +220,6 @@ export default (app, featureFlagRouter: FlagRouter) => {
middlewares.wrap(require("./create-claim-shipment").default) middlewares.wrap(require("./create-claim-shipment").default)
) )
/**
* Delete metadata key / value pair.
*/
route.delete(
"/:id/metadata/:key",
middlewares.wrap(require("./delete-metadata").default)
)
return app return app
} }
@@ -314,7 +301,7 @@ export const defaultAdminOrdersFields = [
"paid_total", "paid_total",
"refundable_amount", "refundable_amount",
"no_notification", "no_notification",
] ] as (keyof Order)[]
export const allowedAdminOrdersFields = [ export const allowedAdminOrdersFields = [
"id", "id",
@@ -393,11 +380,9 @@ export * from "./complete-order"
export * from "./create-claim" export * from "./create-claim"
export * from "./create-claim-shipment" export * from "./create-claim-shipment"
export * from "./create-fulfillment" export * from "./create-fulfillment"
export * from "./create-order"
export * from "./create-shipment" export * from "./create-shipment"
export * from "./create-swap" export * from "./create-swap"
export * from "./create-swap-shipment" export * from "./create-swap-shipment"
export * from "./delete-metadata"
export * from "./fulfill-claim" export * from "./fulfill-claim"
export * from "./fulfill-swap" export * from "./fulfill-swap"
export * from "./get-order" export * from "./get-order"
@@ -3,6 +3,7 @@ import { pick } from "lodash"
import { OrderService } from "../../../../services" import { OrderService } from "../../../../services"
import { AdminListOrdersSelector } from "../../../../types/orders" import { AdminListOrdersSelector } from "../../../../types/orders"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { Order } from "../../../../models"
/** /**
* @oas [get] /orders * @oas [get] /orders
@@ -55,7 +56,7 @@ export default async (req, res) => {
req.listConfig req.listConfig
) )
let data = orders let data: Partial<Order>[] = orders
const fields = [...select, ...relations] const fields = [...select, ...relations]
if (fields.length) { if (fields.length) {
@@ -52,17 +52,17 @@ import { validator } from "../../../../utils/validator"
* type: string * type: string
* payment_method: * payment_method:
* description: * description:
* type: object * type: Record<string, unknown>
* properties: * properties:
* provider_id: * provider_id:
* type: string * type: string
* description: id of the payment provider * description: id of the payment provider
* data: * data:
* description: Data relevant for the given payment method * description: Data relevant for the given payment method
* type: object * type: Record<string, unknown>
* shipping_method: * shipping_method:
* description: The Shipping Method used for shipping the order. * description: The Shipping Method used for shipping the order.
* type: object * type: Record<string, unknown>
* properties: * properties:
* provider_id: * provider_id:
* type: string * type: string
@@ -74,7 +74,7 @@ import { validator } from "../../../../utils/validator"
* type: integer * type: integer
* description: The price of the shipping. * description: The price of the shipping.
* data: * data:
* type: object * type: Record<string, unknown>
* description: Data relevant to the specific shipping method. * description: Data relevant to the specific shipping method.
* items: * items:
* type: array * type: array
@@ -129,7 +129,7 @@ export class AdminPostOrdersOrderReq {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
items?: object[] items?: Record<string, unknown>[]
@IsString() @IsString()
@IsOptional() @IsOptional()
@@ -137,7 +137,7 @@ export class AdminPostOrdersOrderReq {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
discounts?: object[] discounts?: Record<string, unknown>[]
@IsString() @IsString()
@IsOptional() @IsOptional()
@@ -165,7 +165,7 @@ class PaymentMethod {
@IsObject() @IsObject()
@IsOptional() @IsOptional()
data?: object data?: Record<string, unknown>
} }
class ShippingMethod { class ShippingMethod {
@@ -183,9 +183,9 @@ class ShippingMethod {
@IsObject() @IsObject()
@IsOptional() @IsOptional()
data?: object data?: Record<string, unknown>
@IsArray() @IsArray()
@IsOptional() @IsOptional()
items?: object[] items?: Record<string, unknown>[]
} }
@@ -6,6 +6,8 @@ import {
allowedStoreOrdersFields, allowedStoreOrdersFields,
allowedStoreOrdersRelations, allowedStoreOrdersRelations,
} from "../orders" } from "../orders"
import { FindConfig } from "../../../../types/common"
import { Order } from "../../../../models"
/** /**
* @oas [get] /customers/me/orders * @oas [get] /customers/me/orders
@@ -77,7 +79,7 @@ export default async (req, res) => {
skip: validated.offset, skip: validated.offset,
take: validated.limit, take: validated.limit,
order: { created_at: "DESC" }, order: { created_at: "DESC" },
} } as FindConfig<Order>
const [orders, count] = await orderService.listAndCount(selector, listConfig) const [orders, count] = await orderService.listAndCount(selector, listConfig)
@@ -65,7 +65,7 @@ export const defaultStoreOrdersFields = [
"gift_card_total", "gift_card_total",
"subtotal", "subtotal",
"total", "total",
] ] as (keyof Order)[]
export const allowedStoreOrdersRelations = [ export const allowedStoreOrdersRelations = [
"shipping_address", "shipping_address",
+1
View File
@@ -65,4 +65,5 @@ export * from "./store"
export * from "./swap" export * from "./swap"
export * from "./tax-provider" export * from "./tax-provider"
export * from "./tax-rate" export * from "./tax-rate"
export * from "./tracking-link"
export * from "./user" export * from "./user"
+1 -1
View File
@@ -242,7 +242,7 @@ export class Order extends BaseEntity {
// Total fields // Total fields
shipping_total: number shipping_total: number
discount_total: number discount_total: number
tax_total: number tax_total: number | null
refunded_total: number refunded_total: number
total: number total: number
subtotal: number subtotal: number
@@ -7,10 +7,10 @@ import {
ManyToOne, ManyToOne,
Unique, Unique,
} from "typeorm" } from "typeorm"
import { BaseEntity } from "../interfaces/models/base-entity" import { BaseEntity } from "../interfaces"
import { DbAwareColumn } from "../utils/db-aware-column" import { DbAwareColumn } from "../utils/db-aware-column"
import { Cart } from "./cart" import { Cart } from "./cart"
import { generateEntityId } from "../utils/generate-entity-id" import { generateEntityId } from "../utils"
export enum PaymentSessionStatus { export enum PaymentSessionStatus {
AUTHORIZED = "authorized", AUTHORIZED = "authorized",
+2 -2
View File
@@ -5,7 +5,7 @@ import { Order } from "../models"
@EntityRepository(Order) @EntityRepository(Order)
export class OrderRepository extends Repository<Order> { export class OrderRepository extends Repository<Order> {
public async findWithRelations( public async findWithRelations(
relations: Array<keyof Order> = [], relations: string[] = [],
optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {} optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {}
): Promise<Order[]> { ): Promise<Order[]> {
const entities = await this.find(optionsWithoutRelations) const entities = await this.find(optionsWithoutRelations)
@@ -38,7 +38,7 @@ export class OrderRepository extends Repository<Order> {
} }
public async findOneWithRelations( public async findOneWithRelations(
relations: Array<keyof Order> = [], relations: string[] = [],
optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {} optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {}
): Promise<Order> { ): Promise<Order> {
// Limit 1 // Limit 1
@@ -1,6 +1,9 @@
import { IdMap } from "medusa-test-utils" import { IdMap } from "medusa-test-utils"
export const TotalsServiceMock = { export const TotalsServiceMock = {
withTransaction: function () {
return this
},
getTotal: jest.fn().mockImplementation((cart) => { getTotal: jest.fn().mockImplementation((cart) => {
if (cart.total) { if (cart.total) {
return cart.total return cart.total
@@ -15,6 +15,9 @@ const eventBusService = {
describe("CartService", () => { describe("CartService", () => {
const totalsService = { const totalsService = {
withTransaction: function () {
return this
},
getTotal: (o) => { getTotal: (o) => {
return o.total || 0 return o.total || 0
}, },
@@ -4,6 +4,9 @@ import { InventoryServiceMock } from "../__mocks__/inventory"
describe("OrderService", () => { describe("OrderService", () => {
const totalsService = { const totalsService = {
withTransaction: function () {
return this
},
getLineItemRefund: () => {}, getLineItemRefund: () => {},
getTotal: (o) => { getTotal: (o) => {
return o.total || 0 return o.total || 0
@@ -45,35 +48,6 @@ describe("OrderService", () => {
...InventoryServiceMock, ...InventoryServiceMock,
} }
describe("create", () => {
const orderRepo = MockRepository({ create: (f) => f })
const orderService = new OrderService({
manager: MockManager,
orderRepository: orderRepo,
totalsService,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("calls order model functions", async () => {
await orderService.create({
email: "oliver@test.dk",
})
expect(orderRepo.create).toHaveBeenCalledTimes(1)
expect(orderRepo.create).toHaveBeenCalledWith({
email: "oliver@test.dk",
})
expect(orderRepo.save).toHaveBeenCalledWith({
email: "oliver@test.dk",
})
})
})
describe("createFromCart", () => { describe("createFromCart", () => {
const orderRepo = MockRepository({ const orderRepo = MockRepository({
create: (p) => p, create: (p) => p,
@@ -336,7 +310,7 @@ describe("OrderService", () => {
expect(giftCardService.update).toHaveBeenCalledTimes(1) expect(giftCardService.update).toHaveBeenCalledTimes(1)
expect(giftCardService.update).toHaveBeenCalledWith("gid", { expect(giftCardService.update).toHaveBeenCalledWith("gid", {
balance: 0, balance: 0,
disabled: true, is_disabled: true,
}) })
expect(giftCardService.createTransaction).toHaveBeenCalledTimes(1) expect(giftCardService.createTransaction).toHaveBeenCalledTimes(1)
@@ -1,12 +1,5 @@
import { In } from "typeorm"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import ShippingProfileService from "../shipping-profile" import ShippingProfileService from "../shipping-profile"
//import { ShippingProfileModelMock } from "../../models/__mocks__/shipping-profile"
//import { ProductServiceMock, products } from "../__mocks__/product"
//import {
// ShippingOptionServiceMock,
// shippingOptions,
//} from "../__mocks__/shipping-option"
describe("ShippingProfileService", () => { describe("ShippingProfileService", () => {
describe("retrieve", () => { describe("retrieve", () => {
@@ -202,6 +195,9 @@ describe("ShippingProfileService", () => {
} }
const customShippingOptionService = { const customShippingOptionService = {
withTransaction: function () {
return this
},
list: jest.fn().mockImplementation(({ cart_id }, config) => { list: jest.fn().mockImplementation(({ cart_id }, config) => {
if (cart_id === "cso-cart") { if (cart_id === "cso-cart") {
return Promise.resolve([ return Promise.resolve([
@@ -100,7 +100,11 @@ const calculateAdjustment = (cart, lineItem, discount) => {
describe("TotalsService", () => { describe("TotalsService", () => {
const container = { const container = {
taxProviderService: {}, taxProviderService: {
withTransaction: function () {
return this
},
},
taxCalculationStrategy: {}, taxCalculationStrategy: {},
} }
@@ -537,6 +541,9 @@ describe("TotalsService", () => {
const cradle = { const cradle = {
taxProviderService: { taxProviderService: {
withTransaction: function () {
return this
},
getTaxLines: getTaxLinesMock, getTaxLines: getTaxLinesMock,
}, },
taxCalculationStrategy: { taxCalculationStrategy: {
+1 -1
View File
@@ -302,7 +302,7 @@ class FulfillmentService extends TransactionBaseService<FulfillmentService> {
*/ */
async createShipment( async createShipment(
fulfillmentId: string, fulfillmentId: string,
trackingLinks: { tracking_number: string }[], trackingLinks?: { tracking_number: string }[],
config: CreateShipmentConfig = { config: CreateShipmentConfig = {
metadata: {}, metadata: {},
no_notification: undefined, no_notification: undefined,
File diff suppressed because it is too large. Load diff
+4 -6
View File
@@ -407,14 +407,12 @@ class PricingService extends TransactionBaseService<PricingService> {
pricingContext.automatic_taxes && pricingContext.automatic_taxes &&
pricingContext.price_selection.region_id pricingContext.price_selection.region_id
) { ) {
shippingOptionRates = shippingOptionRates = await this.taxProviderService
await this.taxProviderService.getRegionRatesForShipping( .withTransaction(this.manager_)
shippingOption.id, .getRegionRatesForShipping(shippingOption.id, {
{
id: pricingContext.price_selection.region_id, id: pricingContext.price_selection.region_id,
tax_rate: pricingContext.tax_rate, tax_rate: pricingContext.tax_rate,
} })
)
} }
const price = shippingOption.amount || 0 const price = shippingOption.amount || 0
@@ -430,7 +430,9 @@ class ShippingProfileService extends BaseService {
admin_only: false, admin_only: false,
} }
const customShippingOptions = await this.customShippingOptionService_.list( const customShippingOptions = await this.customShippingOptionService_
.withTransaction(this.manager_)
.list(
{ {
cart_id: cart.id, cart_id: cart.id,
}, },
@@ -443,7 +445,9 @@ class ShippingProfileService extends BaseService {
selector.id = customShippingOptions.map((cso) => cso.shipping_option_id) selector.id = customShippingOptions.map((cso) => cso.shipping_option_id)
} }
const rawOpts = await this.shippingOptionService_.list(selector, { const rawOpts = await this.shippingOptionService_
.withTransaction(this.manager_)
.list(selector, {
relations: ["requirements", "profile"], relations: ["requirements", "profile"],
}) })
@@ -464,10 +468,9 @@ class ShippingProfileService extends BaseService {
const options = await Promise.all( const options = await Promise.all(
rawOpts.map(async (so) => { rawOpts.map(async (so) => {
try { try {
const option = await this.shippingOptionService_.validateCartOption( const option = await this.shippingOptionService_
so, .withTransaction(this.manager_)
cart .validateCartOption(so, cart)
)
if (option) { if (option) {
return option return option
} }
+43 -42
View File
@@ -1,30 +1,32 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { AwilixContainer } from "awilix" import { AwilixContainer } from "awilix"
import { BaseService } from "medusa-interfaces" import { EntityManager } from "typeorm"
import { EntityManager, UpdateResult } from "typeorm"
import Redis from "ioredis" import Redis from "ioredis"
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line" import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line"
import { TaxProviderRepository } from "../repositories/tax-provider" import { TaxProviderRepository } from "../repositories/tax-provider"
import { LineItemTaxLine } from "../models/line-item-tax-line" import {
import { TaxProvider } from "../models/tax-provider" LineItemTaxLine,
import { LineItem } from "../models/line-item" TaxProvider,
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line" LineItem,
import { ShippingMethod } from "../models/shipping-method" ShippingMethodTaxLine,
import { Region } from "../models/region" ShippingMethod,
import { Cart } from "../models/cart" Region,
Cart,
} from "../models"
import { isCart } from "../types/cart" import { isCart } from "../types/cart"
import { PostgresError } from "../utils/exception-formatter"
import { import {
ITaxService, ITaxService,
ItemTaxCalculationLine, ItemTaxCalculationLine,
TaxCalculationContext, TaxCalculationContext,
} from "../interfaces/tax-service" TransactionBaseService,
} from "../interfaces"
import { TaxServiceRate } from "../types/tax-service" import { TaxServiceRate } from "../types/tax-service"
import TaxRateService from "./tax-rate" import TaxRateService from "./tax-rate"
import EventBusService from "./event-bus"
const CACHE_TIME = 30 // seconds const CACHE_TIME = 30 // seconds
@@ -36,18 +38,20 @@ type RegionDetails = {
/** /**
* Finds tax providers and assists in tax related operations. * Finds tax providers and assists in tax related operations.
*/ */
class TaxProviderService extends BaseService { class TaxProviderService extends TransactionBaseService<TaxProviderService> {
private container_: AwilixContainer protected manager_: EntityManager
private manager_: EntityManager protected transactionManager_: EntityManager
private transactionManager_: EntityManager
private taxRateService_: TaxRateService protected readonly container_: AwilixContainer
private taxLineRepo_: typeof LineItemTaxLineRepository protected readonly taxRateService_: TaxRateService
private smTaxLineRepo_: typeof ShippingMethodTaxLineRepository protected readonly taxLineRepo_: typeof LineItemTaxLineRepository
private taxProviderRepo_: typeof TaxProviderRepository protected readonly smTaxLineRepo_: typeof ShippingMethodTaxLineRepository
private redis_: Redis protected readonly taxProviderRepo_: typeof TaxProviderRepository
protected readonly redis_: Redis
protected readonly eventBus_: EventBusService
constructor(container: AwilixContainer) { constructor(container: AwilixContainer) {
super() super(container)
this.container_ = container this.container_ = container
this.taxLineRepo_ = container["lineItemTaxLineRepository"] this.taxLineRepo_ = container["lineItemTaxLineRepository"]
@@ -59,19 +63,6 @@ class TaxProviderService extends BaseService {
this.redis_ = container["redisClient"] this.redis_ = container["redisClient"]
} }
withTransaction(transactionManager: EntityManager): TaxProviderService {
if (!transactionManager) {
return this
}
const cloned = new TaxProviderService(this.container_)
cloned.transactionManager_ = transactionManager
cloned.manager_ = transactionManager
return cloned
}
async list(): Promise<TaxProvider[]> { async list(): Promise<TaxProvider[]> {
const tpRepo = this.manager_.getCustomRepository(this.taxProviderRepo_) const tpRepo = this.manager_.getCustomRepository(this.taxProviderRepo_)
return tpRepo.find({}) return tpRepo.find({})
@@ -101,8 +92,11 @@ class TaxProviderService extends BaseService {
} }
async clearTaxLines(cartId: string): Promise<void> { async clearTaxLines(cartId: string): Promise<void> {
const taxLineRepo = this.manager_.getCustomRepository(this.taxLineRepo_) return await this.atomicPhase_(async (transactionManager) => {
const shippingTaxRepo = this.manager_.getCustomRepository( const taxLineRepo = transactionManager.getCustomRepository(
this.taxLineRepo_
)
const shippingTaxRepo = transactionManager.getCustomRepository(
this.smTaxLineRepo_ this.smTaxLineRepo_
) )
@@ -110,6 +104,7 @@ class TaxProviderService extends BaseService {
taxLineRepo.deleteForCart(cartId), taxLineRepo.deleteForCart(cartId),
shippingTaxRepo.deleteForCart(cartId), shippingTaxRepo.deleteForCart(cartId),
]) ])
})
} }
/** /**
@@ -122,6 +117,7 @@ class TaxProviderService extends BaseService {
cartOrLineItems: Cart | LineItem[], cartOrLineItems: Cart | LineItem[],
calculationContext: TaxCalculationContext calculationContext: TaxCalculationContext
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> { ): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
return await this.atomicPhase_(async (transactionManager) => {
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[] = [] let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[] = []
if (isCart(cartOrLineItems)) { if (isCart(cartOrLineItems)) {
taxLines = await this.getTaxLines( taxLines = await this.getTaxLines(
@@ -132,8 +128,10 @@ class TaxProviderService extends BaseService {
taxLines = await this.getTaxLines(cartOrLineItems, calculationContext) taxLines = await this.getTaxLines(cartOrLineItems, calculationContext)
} }
const itemTaxLineRepo = this.manager_.getCustomRepository(this.taxLineRepo_) const itemTaxLineRepo = transactionManager.getCustomRepository(
const shippingTaxLineRepo = this.manager_.getCustomRepository( this.taxLineRepo_
)
const shippingTaxLineRepo = transactionManager.getCustomRepository(
this.smTaxLineRepo_ this.smTaxLineRepo_
) )
@@ -159,6 +157,7 @@ class TaxProviderService extends BaseService {
shippingTaxLineRepo.upsertLines(shipping), shippingTaxLineRepo.upsertLines(shipping),
]) ])
).flat() ).flat()
})
} }
/** /**
@@ -172,11 +171,13 @@ class TaxProviderService extends BaseService {
shippingMethod: ShippingMethod, shippingMethod: ShippingMethod,
calculationContext: TaxCalculationContext calculationContext: TaxCalculationContext
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> { ): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
return await this.atomicPhase_(async (transactionManager) => {
const taxLines = await this.getShippingTaxLines( const taxLines = await this.getShippingTaxLines(
shippingMethod, shippingMethod,
calculationContext calculationContext
) )
return this.manager_.save(taxLines) return await transactionManager.save(taxLines)
})
} }
/** /**
@@ -339,9 +340,9 @@ class TaxProviderService extends BaseService {
} }
let toReturn: TaxServiceRate[] = [] let toReturn: TaxServiceRate[] = []
const optionRates = await this.taxRateService_.listByShippingOption( const optionRates = await this.taxRateService_
optionId .withTransaction(this.manager_)
) .listByShippingOption(optionId)
if (optionRates.length > 0) { if (optionRates.length > 0) {
toReturn = optionRates.map((pr) => { toReturn = optionRates.map((pr) => {
+8 -9
View File
@@ -97,9 +97,9 @@ class TotalsService extends TransactionBaseService<TotalsService> {
private taxCalculationStrategy_: ITaxCalculationStrategy private taxCalculationStrategy_: ITaxCalculationStrategy
constructor({ constructor({
manager,
taxProviderService, taxProviderService,
taxCalculationStrategy, taxCalculationStrategy,
manager,
}: TotalsServiceProps) { }: TotalsServiceProps) {
super({ super({
taxProviderService, taxProviderService,
@@ -107,6 +107,7 @@ class TotalsService extends TransactionBaseService<TotalsService> {
manager, manager,
}) })
this.manager_ = manager
this.taxProviderService_ = taxProviderService this.taxProviderService_ = taxProviderService
this.taxCalculationStrategy_ = taxCalculationStrategy this.taxCalculationStrategy_ = taxCalculationStrategy
this.manager_ = manager this.manager_ = manager
@@ -214,10 +215,9 @@ class TotalsService extends TransactionBaseService<TotalsService> {
taxLines = shippingMethod.tax_lines taxLines = shippingMethod.tax_lines
} else { } else {
const orderLines = await this.taxProviderService_.getTaxLines( const orderLines = await this.taxProviderService_
cartOrOrder.items, .withTransaction(this.manager_)
calculationContext .getTaxLines(cartOrOrder.items, calculationContext)
)
taxLines = orderLines.filter((ol) => { taxLines = orderLines.filter((ol) => {
if ("shipping_method_id" in ol) { if ("shipping_method_id" in ol) {
@@ -343,10 +343,9 @@ class TotalsService extends TransactionBaseService<TotalsService> {
) )
} }
} else { } else {
taxLines = await this.taxProviderService_.getTaxLines( taxLines = await this.taxProviderService_
cartOrOrder.items, .withTransaction(this.manager_)
calculationContext .getTaxLines(cartOrOrder.items, calculationContext)
)
if (cartOrOrder.type === "swap") { if (cartOrOrder.type === "swap") {
const returnTaxLines = cartOrOrder.items.flatMap((i) => { const returnTaxLines = cartOrOrder.items.flatMap((i) => {
@@ -15,6 +15,7 @@ import { BatchJobStatus } from "../../../types/batch-job"
import { prepareListQuery } from "../../../utils/get-query-config" import { prepareListQuery } from "../../../utils/get-query-config"
import { FlagRouter } from "../../../utils/flag-router" import { FlagRouter } from "../../../utils/flag-router"
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
import { FindConfig } from "../../../types/common"
type InjectedDependencies = { type InjectedDependencies = {
fileService: IFileService<never> fileService: IFileService<never>
@@ -146,7 +147,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
skip: offset as number, skip: offset as number,
order: { created_at: "DESC" }, order: { created_at: "DESC" },
take: Math.min(batchJob.context.batch_size ?? Infinity, limit), take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
}) } as FindConfig<Order>)
count = orderCount count = orderCount
} }
@@ -199,11 +200,11 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
order: { created_at: "DESC" }, order: { created_at: "DESC" },
skip: offset, skip: offset,
take: Math.min(batchJob.context.batch_size ?? Infinity, limit), take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
} } as FindConfig<Order>
) )
orderCount = batchJob.context?.batch_size ?? count orderCount = batchJob.context?.batch_size ?? count
let orders = [] let orders: Order[] = []
const lineDescriptor = this.getLineDescriptor( const lineDescriptor = this.getLineDescriptor(
list_config.select as string[], list_config.select as string[],
@@ -230,7 +231,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
...list_config, ...list_config,
skip: offset, skip: offset,
take: Math.min(orderCount - offset, limit), take: Math.min(orderCount - offset, limit),
}) } as FindConfig<Order>)
orders.forEach((order) => { orders.forEach((order) => {
const line = this.buildCSVLine(order, lineDescriptor) const line = this.buildCSVLine(order, lineDescriptor)
@@ -137,7 +137,7 @@ export const orderExportPropertiesDescriptors: OrderDescriptor[] = [
{ {
fieldName: "tax_total", fieldName: "tax_total",
title: "Tax Total", title: "Tax Total",
accessor: (order: Order): string => order.tax_total.toString(), accessor: (order: Order): string => order.tax_total?.toString() ?? "",
}, },
{ {
fieldName: "total", fieldName: "total",
+1 -1
View File
@@ -68,7 +68,7 @@ export type CartUpdateProps = {
billing_address?: AddressPayload | string billing_address?: AddressPayload | string
shipping_address?: AddressPayload | string shipping_address?: AddressPayload | string
completed_at?: Date completed_at?: Date
payment_authorized_at?: Date payment_authorized_at?: Date | null
gift_cards?: GiftCard[] gift_cards?: GiftCard[]
discounts?: Discount[] discounts?: Discount[]
customer_id?: string customer_id?: string
+3 -1
View File
@@ -19,5 +19,7 @@ export type CreateGiftCardTransactionInput = {
gift_card_id: string gift_card_id: string
order_id: string order_id: string
amount: number amount: number
created_at: Date created_at?: Date
is_taxable?: boolean
tax_rate?: number | null
} }
+51 -2
View File
@@ -9,8 +9,8 @@ import {
ValidateNested, ValidateNested,
} from "class-validator" } from "class-validator"
import { IsType } from "../utils/validators/is-type" import { IsType } from "../utils/validators/is-type"
import { Order } from "../models/order" import { Order, Payment } from "../models"
import { DateComparisonOperator } from "./common" import { AddressPayload, DateComparisonOperator } from "./common"
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isOrder(object: any): object is Order { export function isOrder(object: any): object is Order {
@@ -47,6 +47,55 @@ enum PaymentStatus {
requires_action = "requires_action", requires_action = "requires_action",
} }
export type CreateOrderInput = {
status?: OrderStatus
email: string
billing_address: AddressPayload
shipping_address: AddressPayload
items: Record<string, unknown>[]
region: string
discounts?: Record<string, unknown>[]
customer_id: string
payment_method: {
provider_id: string
ata?: Record<string, unknown>
}
shipping_method?: {
provider_id: string
profile_id: string
price: number
data?: Record<string, unknown>
items?: Record<string, unknown>[]
}[]
no_notification?: boolean
}
export type UpdateOrderInput = {
email?: string
billing_address?: AddressPayload
shipping_address?: AddressPayload
items?: object[]
region?: string
discounts?: object[]
customer_id?: string
payment_method?: {
provider_id?: string
data?: Record<string, unknown>
}
shipping_method?: {
provider_id?: string
profile_id?: string
price?: number
data?: Record<string, unknown>
items?: Record<string, unknown>[]
}[]
no_notification?: boolean
payment?: Payment
status?: OrderStatus
fulfillment_status?: FulfillmentStatus
payment_status?: PaymentStatus
metadata?: Record<string, unknown>
}
export class AdminListOrdersSelector { export class AdminListOrdersSelector {
@IsString() @IsString()
@IsOptional() @IsOptional()