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 Peretti
2022-08-01 11:01:17 +02:00
committed by GitHub
parent 1998902a94
commit b54d5178c9
34 changed files with 594 additions and 1092 deletions

View File

@@ -15,21 +15,12 @@ import {
AdminPostOrdersOrderSwapsReq,
AdminPostOrdersOrderSwapsSwapFulfillmentsReq,
AdminPostOrdersOrderSwapsSwapShipmentsReq,
AdminPostOrdersReq,
} from "@medusajs/medusa"
import qs from "qs"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
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(
id: string,
payload: AdminPostOrdersOrderReq,
@@ -261,15 +252,6 @@ class AdminOrdersResource extends BaseResource {
const path = `/admin/orders/${id}/claims/${claimId}/shipments`
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

View File

@@ -7,28 +7,12 @@ import {
AdminPostOrdersOrderReturnsReq,
AdminPostOrdersOrderShipmentReq,
AdminPostOrdersOrderShippingMethodsReq,
AdminPostOrdersReq,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { useMedusa } from "../../../contexts/medusa"
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 = (
id: string,
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)
)
}

View File

@@ -1,7 +1,5 @@
import {
useAdminCreateOrder,
useAdminUpdateOrder,
useAdminDeleteOrderMetadata,
useAdminCompleteOrder,
useAdminCapturePayment,
useAdminRefundPayment,
@@ -16,67 +14,6 @@ import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
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", () => {
test("updates a order and returns it", async () => {
const order = {
@@ -290,23 +227,3 @@ describe("useAdminCancelFulfillment hook", () => {
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"))
})
})

View File

@@ -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: [],
},
],
})
})
})
})

View File

@@ -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"
)
})
})
})

View File

@@ -92,7 +92,7 @@ export class AdminPostOrdersOrderFulfillmentsReq {
@IsObject()
@IsOptional()
metadata?: object
metadata?: Record<string, unknown>
}
class Item {

View File

@@ -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[]
}

View File

@@ -8,6 +8,7 @@ import {
import { defaultAdminOrdersRelations, defaultAdminOrdersFields } from "."
import { OrderService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { TrackingLink } from "../../../../models"
/**
* @oas [post] /orders/{id}/shipment
* operationId: "PostOrdersOrderShipment"
@@ -56,8 +57,13 @@ export default async (req, res) => {
await orderService.createShipment(
id,
validated.fulfillment_id,
validated.tracking_numbers?.map((n) => ({ tracking_number: n })),
{ no_notification: validated.no_notification }
validated.tracking_numbers?.map((n) => ({
tracking_number: n,
})) as TrackingLink[],
{
metadata: {},
no_notification: validated.no_notification,
}
)
const order = await orderService.retrieve(id, {

View File

@@ -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 })
}

View File

@@ -48,11 +48,6 @@ export default (app, featureFlagRouter: FlagRouter) => {
middlewares.wrap(require("./get-order").default)
)
/**
* Create a new order
*/
route.post("/", middlewares.wrap(require("./create-order").default))
/**
* Update an order
*/
@@ -225,14 +220,6 @@ export default (app, featureFlagRouter: FlagRouter) => {
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
}
@@ -314,7 +301,7 @@ export const defaultAdminOrdersFields = [
"paid_total",
"refundable_amount",
"no_notification",
]
] as (keyof Order)[]
export const allowedAdminOrdersFields = [
"id",
@@ -393,11 +380,9 @@ export * from "./complete-order"
export * from "./create-claim"
export * from "./create-claim-shipment"
export * from "./create-fulfillment"
export * from "./create-order"
export * from "./create-shipment"
export * from "./create-swap"
export * from "./create-swap-shipment"
export * from "./delete-metadata"
export * from "./fulfill-claim"
export * from "./fulfill-swap"
export * from "./get-order"

View File

@@ -3,6 +3,7 @@ import { pick } from "lodash"
import { OrderService } from "../../../../services"
import { AdminListOrdersSelector } from "../../../../types/orders"
import { Type } from "class-transformer"
import { Order } from "../../../../models"
/**
* @oas [get] /orders
@@ -55,7 +56,7 @@ export default async (req, res) => {
req.listConfig
)
let data = orders
let data: Partial<Order>[] = orders
const fields = [...select, ...relations]
if (fields.length) {

View File

@@ -52,17 +52,17 @@ import { validator } from "../../../../utils/validator"
* type: string
* payment_method:
* description:
* type: object
* type: Record<string, unknown>
* properties:
* provider_id:
* type: string
* description: id of the payment provider
* data:
* description: Data relevant for the given payment method
* type: object
* type: Record<string, unknown>
* shipping_method:
* description: The Shipping Method used for shipping the order.
* type: object
* type: Record<string, unknown>
* properties:
* provider_id:
* type: string
@@ -74,7 +74,7 @@ import { validator } from "../../../../utils/validator"
* type: integer
* description: The price of the shipping.
* data:
* type: object
* type: Record<string, unknown>
* description: Data relevant to the specific shipping method.
* items:
* type: array
@@ -129,7 +129,7 @@ export class AdminPostOrdersOrderReq {
@IsArray()
@IsOptional()
items?: object[]
items?: Record<string, unknown>[]
@IsString()
@IsOptional()
@@ -137,7 +137,7 @@ export class AdminPostOrdersOrderReq {
@IsArray()
@IsOptional()
discounts?: object[]
discounts?: Record<string, unknown>[]
@IsString()
@IsOptional()
@@ -165,7 +165,7 @@ class PaymentMethod {
@IsObject()
@IsOptional()
data?: object
data?: Record<string, unknown>
}
class ShippingMethod {
@@ -183,9 +183,9 @@ class ShippingMethod {
@IsObject()
@IsOptional()
data?: object
data?: Record<string, unknown>
@IsArray()
@IsOptional()
items?: object[]
items?: Record<string, unknown>[]
}

View File

@@ -6,6 +6,8 @@ import {
allowedStoreOrdersFields,
allowedStoreOrdersRelations,
} from "../orders"
import { FindConfig } from "../../../../types/common"
import { Order } from "../../../../models"
/**
* @oas [get] /customers/me/orders
@@ -77,7 +79,7 @@ export default async (req, res) => {
skip: validated.offset,
take: validated.limit,
order: { created_at: "DESC" },
}
} as FindConfig<Order>
const [orders, count] = await orderService.listAndCount(selector, listConfig)

View File

@@ -65,7 +65,7 @@ export const defaultStoreOrdersFields = [
"gift_card_total",
"subtotal",
"total",
]
] as (keyof Order)[]
export const allowedStoreOrdersRelations = [
"shipping_address",

View File

@@ -65,4 +65,5 @@ export * from "./store"
export * from "./swap"
export * from "./tax-provider"
export * from "./tax-rate"
export * from "./tracking-link"
export * from "./user"

View File

@@ -242,7 +242,7 @@ export class Order extends BaseEntity {
// Total fields
shipping_total: number
discount_total: number
tax_total: number
tax_total: number | null
refunded_total: number
total: number
subtotal: number

View File

@@ -7,10 +7,10 @@ import {
ManyToOne,
Unique,
} from "typeorm"
import { BaseEntity } from "../interfaces/models/base-entity"
import { BaseEntity } from "../interfaces"
import { DbAwareColumn } from "../utils/db-aware-column"
import { Cart } from "./cart"
import { generateEntityId } from "../utils/generate-entity-id"
import { generateEntityId } from "../utils"
export enum PaymentSessionStatus {
AUTHORIZED = "authorized",

View File

@@ -5,7 +5,7 @@ import { Order } from "../models"
@EntityRepository(Order)
export class OrderRepository extends Repository<Order> {
public async findWithRelations(
relations: Array<keyof Order> = [],
relations: string[] = [],
optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {}
): Promise<Order[]> {
const entities = await this.find(optionsWithoutRelations)
@@ -38,7 +38,7 @@ export class OrderRepository extends Repository<Order> {
}
public async findOneWithRelations(
relations: Array<keyof Order> = [],
relations: string[] = [],
optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {}
): Promise<Order> {
// Limit 1

View File

@@ -1,6 +1,9 @@
import { IdMap } from "medusa-test-utils"
export const TotalsServiceMock = {
withTransaction: function () {
return this
},
getTotal: jest.fn().mockImplementation((cart) => {
if (cart.total) {
return cart.total

View File

@@ -15,6 +15,9 @@ const eventBusService = {
describe("CartService", () => {
const totalsService = {
withTransaction: function () {
return this
},
getTotal: (o) => {
return o.total || 0
},

View File

@@ -4,6 +4,9 @@ import { InventoryServiceMock } from "../__mocks__/inventory"
describe("OrderService", () => {
const totalsService = {
withTransaction: function () {
return this
},
getLineItemRefund: () => {},
getTotal: (o) => {
return o.total || 0
@@ -45,35 +48,6 @@ describe("OrderService", () => {
...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", () => {
const orderRepo = MockRepository({
create: (p) => p,
@@ -336,7 +310,7 @@ describe("OrderService", () => {
expect(giftCardService.update).toHaveBeenCalledTimes(1)
expect(giftCardService.update).toHaveBeenCalledWith("gid", {
balance: 0,
disabled: true,
is_disabled: true,
})
expect(giftCardService.createTransaction).toHaveBeenCalledTimes(1)

View File

@@ -1,12 +1,5 @@
import { In } from "typeorm"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
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("retrieve", () => {
@@ -202,6 +195,9 @@ describe("ShippingProfileService", () => {
}
const customShippingOptionService = {
withTransaction: function () {
return this
},
list: jest.fn().mockImplementation(({ cart_id }, config) => {
if (cart_id === "cso-cart") {
return Promise.resolve([

View File

@@ -100,7 +100,11 @@ const calculateAdjustment = (cart, lineItem, discount) => {
describe("TotalsService", () => {
const container = {
taxProviderService: {},
taxProviderService: {
withTransaction: function () {
return this
},
},
taxCalculationStrategy: {},
}
@@ -537,6 +541,9 @@ describe("TotalsService", () => {
const cradle = {
taxProviderService: {
withTransaction: function () {
return this
},
getTaxLines: getTaxLinesMock,
},
taxCalculationStrategy: {

View File

@@ -302,7 +302,7 @@ class FulfillmentService extends TransactionBaseService<FulfillmentService> {
*/
async createShipment(
fulfillmentId: string,
trackingLinks: { tracking_number: string }[],
trackingLinks?: { tracking_number: string }[],
config: CreateShipmentConfig = {
metadata: {},
no_notification: undefined,

View File

@@ -407,14 +407,12 @@ class PricingService extends TransactionBaseService<PricingService> {
pricingContext.automatic_taxes &&
pricingContext.price_selection.region_id
) {
shippingOptionRates =
await this.taxProviderService.getRegionRatesForShipping(
shippingOption.id,
{
id: pricingContext.price_selection.region_id,
tax_rate: pricingContext.tax_rate,
}
)
shippingOptionRates = await this.taxProviderService
.withTransaction(this.manager_)
.getRegionRatesForShipping(shippingOption.id, {
id: pricingContext.price_selection.region_id,
tax_rate: pricingContext.tax_rate,
})
}
const price = shippingOption.amount || 0

View File

@@ -430,12 +430,14 @@ class ShippingProfileService extends BaseService {
admin_only: false,
}
const customShippingOptions = await this.customShippingOptionService_.list(
{
cart_id: cart.id,
},
{ select: ["id", "shipping_option_id", "price"] }
)
const customShippingOptions = await this.customShippingOptionService_
.withTransaction(this.manager_)
.list(
{
cart_id: cart.id,
},
{ select: ["id", "shipping_option_id", "price"] }
)
const hasCustomShippingOptions = customShippingOptions?.length
// if there are custom shipping options associated with the cart, use those
@@ -443,9 +445,11 @@ class ShippingProfileService extends BaseService {
selector.id = customShippingOptions.map((cso) => cso.shipping_option_id)
}
const rawOpts = await this.shippingOptionService_.list(selector, {
relations: ["requirements", "profile"],
})
const rawOpts = await this.shippingOptionService_
.withTransaction(this.manager_)
.list(selector, {
relations: ["requirements", "profile"],
})
// if there are custom shipping options associated with the cart, return cart shipping options with custom price
if (hasCustomShippingOptions) {
@@ -464,10 +468,9 @@ class ShippingProfileService extends BaseService {
const options = await Promise.all(
rawOpts.map(async (so) => {
try {
const option = await this.shippingOptionService_.validateCartOption(
so,
cart
)
const option = await this.shippingOptionService_
.withTransaction(this.manager_)
.validateCartOption(so, cart)
if (option) {
return option
}

View File

@@ -1,30 +1,32 @@
import { MedusaError } from "medusa-core-utils"
import { AwilixContainer } from "awilix"
import { BaseService } from "medusa-interfaces"
import { EntityManager, UpdateResult } from "typeorm"
import { EntityManager } from "typeorm"
import Redis from "ioredis"
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line"
import { TaxProviderRepository } from "../repositories/tax-provider"
import { LineItemTaxLine } from "../models/line-item-tax-line"
import { TaxProvider } from "../models/tax-provider"
import { LineItem } from "../models/line-item"
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
import { ShippingMethod } from "../models/shipping-method"
import { Region } from "../models/region"
import { Cart } from "../models/cart"
import {
LineItemTaxLine,
TaxProvider,
LineItem,
ShippingMethodTaxLine,
ShippingMethod,
Region,
Cart,
} from "../models"
import { isCart } from "../types/cart"
import { PostgresError } from "../utils/exception-formatter"
import {
ITaxService,
ItemTaxCalculationLine,
TaxCalculationContext,
} from "../interfaces/tax-service"
TransactionBaseService,
} from "../interfaces"
import { TaxServiceRate } from "../types/tax-service"
import TaxRateService from "./tax-rate"
import EventBusService from "./event-bus"
const CACHE_TIME = 30 // seconds
@@ -36,18 +38,20 @@ type RegionDetails = {
/**
* Finds tax providers and assists in tax related operations.
*/
class TaxProviderService extends BaseService {
private container_: AwilixContainer
private manager_: EntityManager
private transactionManager_: EntityManager
private taxRateService_: TaxRateService
private taxLineRepo_: typeof LineItemTaxLineRepository
private smTaxLineRepo_: typeof ShippingMethodTaxLineRepository
private taxProviderRepo_: typeof TaxProviderRepository
private redis_: Redis
class TaxProviderService extends TransactionBaseService<TaxProviderService> {
protected manager_: EntityManager
protected transactionManager_: EntityManager
protected readonly container_: AwilixContainer
protected readonly taxRateService_: TaxRateService
protected readonly taxLineRepo_: typeof LineItemTaxLineRepository
protected readonly smTaxLineRepo_: typeof ShippingMethodTaxLineRepository
protected readonly taxProviderRepo_: typeof TaxProviderRepository
protected readonly redis_: Redis
protected readonly eventBus_: EventBusService
constructor(container: AwilixContainer) {
super()
super(container)
this.container_ = container
this.taxLineRepo_ = container["lineItemTaxLineRepository"]
@@ -59,19 +63,6 @@ class TaxProviderService extends BaseService {
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[]> {
const tpRepo = this.manager_.getCustomRepository(this.taxProviderRepo_)
return tpRepo.find({})
@@ -101,15 +92,19 @@ class TaxProviderService extends BaseService {
}
async clearTaxLines(cartId: string): Promise<void> {
const taxLineRepo = this.manager_.getCustomRepository(this.taxLineRepo_)
const shippingTaxRepo = this.manager_.getCustomRepository(
this.smTaxLineRepo_
)
return await this.atomicPhase_(async (transactionManager) => {
const taxLineRepo = transactionManager.getCustomRepository(
this.taxLineRepo_
)
const shippingTaxRepo = transactionManager.getCustomRepository(
this.smTaxLineRepo_
)
await Promise.all([
taxLineRepo.deleteForCart(cartId),
shippingTaxRepo.deleteForCart(cartId),
])
await Promise.all([
taxLineRepo.deleteForCart(cartId),
shippingTaxRepo.deleteForCart(cartId),
])
})
}
/**
@@ -122,43 +117,47 @@ class TaxProviderService extends BaseService {
cartOrLineItems: Cart | LineItem[],
calculationContext: TaxCalculationContext
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[] = []
if (isCart(cartOrLineItems)) {
taxLines = await this.getTaxLines(
cartOrLineItems.items,
calculationContext
return await this.atomicPhase_(async (transactionManager) => {
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[] = []
if (isCart(cartOrLineItems)) {
taxLines = await this.getTaxLines(
cartOrLineItems.items,
calculationContext
)
} else {
taxLines = await this.getTaxLines(cartOrLineItems, calculationContext)
}
const itemTaxLineRepo = transactionManager.getCustomRepository(
this.taxLineRepo_
)
const shippingTaxLineRepo = transactionManager.getCustomRepository(
this.smTaxLineRepo_
)
} else {
taxLines = await this.getTaxLines(cartOrLineItems, calculationContext)
}
const itemTaxLineRepo = this.manager_.getCustomRepository(this.taxLineRepo_)
const shippingTaxLineRepo = this.manager_.getCustomRepository(
this.smTaxLineRepo_
)
const { shipping, lineItems } = taxLines.reduce<{
shipping: ShippingMethodTaxLine[]
lineItems: LineItemTaxLine[]
}>(
(acc, tl) => {
if ("item_id" in tl) {
acc.lineItems.push(tl)
} else {
acc.shipping.push(tl)
}
const { shipping, lineItems } = taxLines.reduce<{
shipping: ShippingMethodTaxLine[]
lineItems: LineItemTaxLine[]
}>(
(acc, tl) => {
if ("item_id" in tl) {
acc.lineItems.push(tl)
} else {
acc.shipping.push(tl)
}
return acc
},
{ shipping: [], lineItems: [] }
)
return acc
},
{ shipping: [], lineItems: [] }
)
return (
await Promise.all([
itemTaxLineRepo.upsertLines(lineItems),
shippingTaxLineRepo.upsertLines(shipping),
])
).flat()
return (
await Promise.all([
itemTaxLineRepo.upsertLines(lineItems),
shippingTaxLineRepo.upsertLines(shipping),
])
).flat()
})
}
/**
@@ -172,11 +171,13 @@ class TaxProviderService extends BaseService {
shippingMethod: ShippingMethod,
calculationContext: TaxCalculationContext
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
const taxLines = await this.getShippingTaxLines(
shippingMethod,
calculationContext
)
return this.manager_.save(taxLines)
return await this.atomicPhase_(async (transactionManager) => {
const taxLines = await this.getShippingTaxLines(
shippingMethod,
calculationContext
)
return await transactionManager.save(taxLines)
})
}
/**
@@ -339,9 +340,9 @@ class TaxProviderService extends BaseService {
}
let toReturn: TaxServiceRate[] = []
const optionRates = await this.taxRateService_.listByShippingOption(
optionId
)
const optionRates = await this.taxRateService_
.withTransaction(this.manager_)
.listByShippingOption(optionId)
if (optionRates.length > 0) {
toReturn = optionRates.map((pr) => {

View File

@@ -97,9 +97,9 @@ class TotalsService extends TransactionBaseService<TotalsService> {
private taxCalculationStrategy_: ITaxCalculationStrategy
constructor({
manager,
taxProviderService,
taxCalculationStrategy,
manager,
}: TotalsServiceProps) {
super({
taxProviderService,
@@ -107,6 +107,7 @@ class TotalsService extends TransactionBaseService<TotalsService> {
manager,
})
this.manager_ = manager
this.taxProviderService_ = taxProviderService
this.taxCalculationStrategy_ = taxCalculationStrategy
this.manager_ = manager
@@ -214,10 +215,9 @@ class TotalsService extends TransactionBaseService<TotalsService> {
taxLines = shippingMethod.tax_lines
} else {
const orderLines = await this.taxProviderService_.getTaxLines(
cartOrOrder.items,
calculationContext
)
const orderLines = await this.taxProviderService_
.withTransaction(this.manager_)
.getTaxLines(cartOrOrder.items, calculationContext)
taxLines = orderLines.filter((ol) => {
if ("shipping_method_id" in ol) {
@@ -343,10 +343,9 @@ class TotalsService extends TransactionBaseService<TotalsService> {
)
}
} else {
taxLines = await this.taxProviderService_.getTaxLines(
cartOrOrder.items,
calculationContext
)
taxLines = await this.taxProviderService_
.withTransaction(this.manager_)
.getTaxLines(cartOrOrder.items, calculationContext)
if (cartOrOrder.type === "swap") {
const returnTaxLines = cartOrOrder.items.flatMap((i) => {

View File

@@ -15,6 +15,7 @@ import { BatchJobStatus } from "../../../types/batch-job"
import { prepareListQuery } from "../../../utils/get-query-config"
import { FlagRouter } from "../../../utils/flag-router"
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
import { FindConfig } from "../../../types/common"
type InjectedDependencies = {
fileService: IFileService<never>
@@ -146,7 +147,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
skip: offset as number,
order: { created_at: "DESC" },
take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
})
} as FindConfig<Order>)
count = orderCount
}
@@ -199,11 +200,11 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
order: { created_at: "DESC" },
skip: offset,
take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
}
} as FindConfig<Order>
)
orderCount = batchJob.context?.batch_size ?? count
let orders = []
let orders: Order[] = []
const lineDescriptor = this.getLineDescriptor(
list_config.select as string[],
@@ -230,7 +231,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
...list_config,
skip: offset,
take: Math.min(orderCount - offset, limit),
})
} as FindConfig<Order>)
orders.forEach((order) => {
const line = this.buildCSVLine(order, lineDescriptor)

View File

@@ -137,7 +137,7 @@ export const orderExportPropertiesDescriptors: OrderDescriptor[] = [
{
fieldName: "tax_total",
title: "Tax Total",
accessor: (order: Order): string => order.tax_total.toString(),
accessor: (order: Order): string => order.tax_total?.toString() ?? "",
},
{
fieldName: "total",

View File

@@ -68,7 +68,7 @@ export type CartUpdateProps = {
billing_address?: AddressPayload | string
shipping_address?: AddressPayload | string
completed_at?: Date
payment_authorized_at?: Date
payment_authorized_at?: Date | null
gift_cards?: GiftCard[]
discounts?: Discount[]
customer_id?: string

View File

@@ -19,5 +19,7 @@ export type CreateGiftCardTransactionInput = {
gift_card_id: string
order_id: string
amount: number
created_at: Date
created_at?: Date
is_taxable?: boolean
tax_rate?: number | null
}

View File

@@ -9,8 +9,8 @@ import {
ValidateNested,
} from "class-validator"
import { IsType } from "../utils/validators/is-type"
import { Order } from "../models/order"
import { DateComparisonOperator } from "./common"
import { Order, Payment } from "../models"
import { AddressPayload, DateComparisonOperator } from "./common"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isOrder(object: any): object is Order {
@@ -47,6 +47,55 @@ enum PaymentStatus {
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 {
@IsString()
@IsOptional()