fix(medusa): Allow AddressPayload or string on DraftOrder creation (#1902)

This commit is contained in:
Kasper Fabricius Kristensen
2022-10-19 18:01:08 +02:00
committed by GitHub
parent 8be67c734c
commit fcfb7d167b
14 changed files with 385 additions and 66 deletions

View File

@@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/admin/draft-orders POST /admin/draft-orders creates a draft order with a billing address that is an AddressPayload and a shipping address that is an ID 1`] = `
Object {
"address_1": null,
"address_2": null,
"city": null,
"company": null,
"country_code": "us",
"created_at": Any<String>,
"customer_id": null,
"deleted_at": null,
"first_name": "oli",
"id": "oli-shipping",
"last_name": "test",
"metadata": null,
"phone": null,
"postal_code": null,
"province": null,
"updated_at": Any<String>,
}
`;
exports[`/admin/draft-orders POST /admin/draft-orders creates a draft order with a billing address that is an AddressPayload and a shipping address that is an ID 2`] = `
Object {
"address_1": null,
"address_2": null,
"city": null,
"company": null,
"country_code": "us",
"created_at": Any<String>,
"customer_id": null,
"deleted_at": null,
"first_name": "kap",
"id": Any<String>,
"last_name": "test",
"metadata": null,
"phone": null,
"postal_code": null,
"province": null,
"updated_at": Any<String>,
}
`;
exports[`/admin/draft-orders POST /admin/draft-orders creates a draft order with a shipping address that is an AddressPayload and a billing adress that is an ID 1`] = `
Object {
"address_1": null,
"address_2": null,
"city": null,
"company": null,
"country_code": "us",
"created_at": Any<String>,
"customer_id": null,
"deleted_at": null,
"first_name": "oli",
"id": "oli-shipping",
"last_name": "test",
"metadata": null,
"phone": null,
"postal_code": null,
"province": null,
"updated_at": Any<String>,
}
`;
exports[`/admin/draft-orders POST /admin/draft-orders creates a draft order with a shipping address that is an AddressPayload and a billing adress that is an ID 2`] = `
Object {
"address_1": null,
"address_2": null,
"city": null,
"company": null,
"country_code": "us",
"created_at": Any<String>,
"customer_id": null,
"deleted_at": null,
"first_name": "kap",
"id": Any<String>,
"last_name": "test",
"metadata": null,
"phone": null,
"postal_code": null,
"province": null,
"updated_at": Any<String>,
}
`;

View File

@@ -71,6 +71,150 @@ describe("/admin/draft-orders", () => {
expect(response.status).toEqual(200)
})
it("creates a draft order with a billing address that is an AddressPayload and a shipping address that is an ID", async () => {
const api = useApi()
const payload = {
email: "oli@test.dk",
billing_address: {
first_name: "kap",
last_name: "test",
country_code: "us",
},
shipping_address: "oli-shipping",
items: [
{
variant_id: "test-variant",
quantity: 2,
metadata: {},
},
],
region_id: "test-region",
customer_id: "oli-test",
shipping_methods: [
{
option_id: "test-option",
},
],
}
const {
status,
data: { draft_order },
} = await api
.post("/admin/draft-orders", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(status).toEqual(200)
expect(draft_order.cart.billing_address_id).not.toBeNull()
expect(draft_order.cart.shipping_address_id).not.toBeNull()
const afterCreate = await api.get(
`/admin/draft-orders/${draft_order.id}`,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
expect(
afterCreate.data.draft_order.cart.shipping_address
).toMatchSnapshot({
id: "oli-shipping",
created_at: expect.any(String),
updated_at: expect.any(String),
})
expect(afterCreate.data.draft_order.cart.billing_address).toMatchSnapshot(
{
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
first_name: "kap",
last_name: "test",
country_code: "us",
}
)
})
it("creates a draft order with a shipping address that is an AddressPayload and a billing adress that is an ID", async () => {
const api = useApi()
const payload = {
email: "oli@test.dk",
shipping_address: {
first_name: "kap",
last_name: "test",
country_code: "us",
},
billing_address: "oli-shipping",
items: [
{
variant_id: "test-variant",
quantity: 2,
metadata: {},
},
],
region_id: "test-region",
customer_id: "oli-test",
shipping_methods: [
{
option_id: "test-option",
},
],
}
const {
status,
data: { draft_order },
} = await api
.post("/admin/draft-orders", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(status).toEqual(200)
expect(draft_order.cart.billing_address_id).not.toBeNull()
expect(draft_order.cart.shipping_address_id).not.toBeNull()
const afterCreate = await api.get(
`/admin/draft-orders/${draft_order.id}`,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
expect(afterCreate.data.draft_order.cart.billing_address).toMatchSnapshot(
{
id: "oli-shipping",
created_at: expect.any(String),
updated_at: expect.any(String),
}
)
expect(
afterCreate.data.draft_order.cart.shipping_address
).toMatchSnapshot({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
first_name: "kap",
last_name: "test",
country_code: "us",
})
})
it("creates a draft order cart and creates new user", async () => {
const api = useApi()

View File

@@ -7,6 +7,7 @@ import {
AdminPostDraftOrdersDraftOrderLineItemsReq,
AdminPostDraftOrdersDraftOrderRegisterPaymentRes,
AdminPostDraftOrdersDraftOrderReq,
AdminPostDraftOrdersReq,
} from "@medusajs/medusa"
import qs from "qs"
import { ResponsePromise } from "../../typings"
@@ -17,9 +18,8 @@ class AdminDraftOrdersResource extends BaseResource {
* @description Creates a draft order
*/
create(
payload: AdminPostDraftOrdersDraftOrderReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminDraftOrdersRes> {
payload: AdminPostDraftOrdersReq,
customHeaders: Record<string, any> = {}): ResponsePromise<AdminDraftOrdersRes> {
const path = `/admin/draft-orders`
return this.client.request("POST", path, payload, {}, customHeaders)
}
@@ -95,7 +95,7 @@ class AdminDraftOrdersResource extends BaseResource {
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPostDraftOrdersDraftOrderRegisterPaymentRes> {
const path = `/admin/draft-orders/${id}/pay`
return this.client.request("POST", path, undefined)
return this.client.request("POST", path, {}, customHeaders)
}
/**

View File

@@ -5,7 +5,7 @@ import {
AdminPostDraftOrdersDraftOrderLineItemsReq,
AdminPostDraftOrdersDraftOrderRegisterPaymentRes,
AdminPostDraftOrdersDraftOrderReq,
AdminPostDraftOrdersReq,
AdminPostDraftOrdersReq
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"

View File

@@ -28,8 +28,8 @@ export * from "./routes/admin/gift-cards"
export * from "./routes/admin/invites"
export * from "./routes/admin/notes"
export * from "./routes/admin/notifications"
export * from "./routes/admin/orders"
export * from "./routes/admin/order-edits"
export * from "./routes/admin/orders"
export * from "./routes/admin/price-lists"
export * from "./routes/admin/product-tags"
export * from "./routes/admin/product-types"
@@ -52,8 +52,8 @@ export * from "./routes/store/carts"
export * from "./routes/store/collections"
export * from "./routes/store/customers"
export * from "./routes/store/gift-cards"
export * from "./routes/store/orders"
export * from "./routes/store/order-edits"
export * from "./routes/store/orders"
export * from "./routes/store/products"
export * from "./routes/store/regions"
export * from "./routes/store/return-reasons"

View File

@@ -15,14 +15,14 @@ import {
defaultAdminDraftOrdersRelations,
} from "."
import { AddressPayload } from "../../../../types/common"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import { DraftOrder } from "../../../.."
import { DraftOrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { Type } from "class-transformer"
import { transformIdableFields } from "medusa-core-utils"
import { AddressPayload } from "../../../../types/common"
import { DraftOrderCreateProps } from "../../../../types/draft-orders"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
/**
* @oas [post] /draft-orders
* operationId: "PostDraftOrders"
@@ -49,10 +49,14 @@ import { validator } from "../../../../utils/validator"
* format: email
* billing_address:
* description: "The Address to be used for billing purposes."
* $ref: "#/components/schemas/address_fields"
* anyOf:
* - $ref: "#/components/schemas/address_fields"
* - type: string
* shipping_address:
* description: "The Address to be used for shipping."
* $ref: "#/components/schemas/address_fields"
* anyOf:
* - $ref: "#/components/schemas/address_fields"
* - type: string
* items:
* description: The Line Items that have been received.
* type: array
@@ -191,10 +195,21 @@ import { validator } from "../../../../utils/validator"
export default async (req, res) => {
const validated = await validator(AdminPostDraftOrdersReq, req.body)
const value = transformIdableFields(validated, [
"shipping_address",
"billing_address",
])
const { shipping_address, billing_address, ...rest } = validated
const draftOrderDataToCreate: DraftOrderCreateProps = { ...rest }
if (typeof shipping_address === "string") {
draftOrderDataToCreate.shipping_address_id = shipping_address
} else {
draftOrderDataToCreate.shipping_address = shipping_address
}
if (typeof billing_address === "string") {
draftOrderDataToCreate.billing_address_id = billing_address
} else {
draftOrderDataToCreate.billing_address = billing_address
}
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
@@ -204,7 +219,7 @@ export default async (req, res) => {
async (transactionManager) => {
return await draftOrderService
.withTransaction(transactionManager)
.create(value)
.create(draftOrderDataToCreate)
}
)
@@ -230,12 +245,12 @@ export class AdminPostDraftOrdersReq {
email: string
@IsOptional()
@Type(() => AddressPayload)
billing_address?: AddressPayload
@IsType([AddressPayload, String])
billing_address?: AddressPayload | string
@IsOptional()
@Type(() => AddressPayload)
shipping_address?: AddressPayload
@IsType([AddressPayload, String])
shipping_address?: AddressPayload | string
@IsArray()
@Type(() => Item)

View File

@@ -1,4 +1,4 @@
import { CartService, DraftOrderService } from "../../../../services"
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
@@ -7,18 +7,18 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import {
defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations,
} from "."
import { AddressPayload } from "../../../../types/common"
import { DraftOrderStatus } from "../../../../models"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Type } from "class-transformer"
import { CartService, DraftOrderService } from "../../../../services"
import { CartUpdateProps } from "../../../../types/cart"
import { AddressPayload } from "../../../../types/common"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
/**
* @oas [post] /admin/draft-orders/{id}
* operationId: PostDraftOrdersDraftOrder
@@ -47,10 +47,14 @@ import { validator } from "../../../../utils/validator"
* format: email
* billing_address:
* description: "The Address to be used for billing purposes."
* $ref: "#/components/schemas/address_fields"
* anyOf:
* - $ref: "#/components/schemas/address_fields"
* - type: string
* shipping_address:
* description: "The Address to be used for shipping."
* $ref: "#/components/schemas/address_fields"
* anyOf:
* - $ref: "#/components/schemas/address_fields"
* - type: string
* discounts:
* description: "An array of Discount codes to add to the Draft Order."
* type: array
@@ -125,6 +129,7 @@ export default async (req, res) => {
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const cartService: CartService = req.scope.resolve("cartService")
const draftOrder = await draftOrderService.retrieve(id)
@@ -147,9 +152,23 @@ export default async (req, res) => {
delete validated.no_notification_order
}
await cartService
.withTransaction(transactionManager)
.update(draftOrder.cart_id, validated)
const { shipping_address, billing_address, ...rest } = validated
const cartDataToUpdate: CartUpdateProps = { ...rest }
if (typeof shipping_address === "string") {
cartDataToUpdate.shipping_address_id = shipping_address
} else {
cartDataToUpdate.shipping_address = shipping_address
}
if (typeof billing_address === "string") {
cartDataToUpdate.billing_address_id = billing_address
} else {
cartDataToUpdate.billing_address = billing_address
}
await cartService.update(draftOrder.cart_id, cartDataToUpdate)
})
draftOrder.cart = await cartService.retrieve(draftOrder.cart_id, {
@@ -174,12 +193,12 @@ export class AdminPostDraftOrdersDraftOrderReq {
email?: string
@IsOptional()
@Type(() => AddressPayload)
billing_address?: AddressPayload
@IsType([AddressPayload, String])
billing_address?: AddressPayload | string
@IsOptional()
@Type(() => AddressPayload)
shipping_address?: AddressPayload
@IsType([AddressPayload, String])
shipping_address?: AddressPayload | string
@IsArray()
@IsOptional()

View File

@@ -14,8 +14,8 @@ import giftCardRoutes from "./gift-cards"
import inviteRoutes, { unauthenticatedInviteRoutes } from "./invites"
import noteRoutes from "./notes"
import notificationRoutes from "./notifications"
import orderRoutes from "./orders"
import orderEditRoutes from "./order-edits"
import orderRoutes from "./orders"
import priceListRoutes from "./price-lists"
import productTagRoutes from "./product-tags"
import productTypesRoutes from "./product-types"

View File

@@ -7,13 +7,13 @@ import {
} from "class-validator"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { AddressPayload } from "../../../../types/common"
import { CartService } from "../../../../services"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { CartService } from "../../../../services"
import { AddressPayload } from "../../../../types/common"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { IsType } from "../../../../utils/validators/is-type"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { Type } from "class-transformer"
/**
* @oas [post] /carts/{id}

View File

@@ -38,9 +38,9 @@ export * from "./order"
export * from "./order-edit"
export * from "./order-item-change"
export * from "./payment"
export * from "./payment-collection"
export * from "./payment-provider"
export * from "./payment-session"
export * from "./payment-collection"
export * from "./price-list"
export * from "./product"
export * from "./product-collection"

View File

@@ -2,6 +2,7 @@ import { isEmpty, isEqual } from "lodash"
import { MedusaError } from "medusa-core-utils"
import { DeepPartial, EntityManager, In } from "typeorm"
import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import {
Address,
Cart,
@@ -34,6 +35,7 @@ import CustomerService from "./customer"
import DiscountService from "./discount"
import EventBusService from "./event-bus"
import GiftCardService from "./gift-card"
import { SalesChannelService } from "./index"
import InventoryService from "./inventory"
import LineItemService from "./line-item"
import LineItemAdjustmentService from "./line-item-adjustment"
@@ -42,11 +44,9 @@ import ProductService from "./product"
import ProductVariantService from "./product-variant"
import RegionService from "./region"
import ShippingOptionService from "./shipping-option"
import StoreService from "./store"
import TaxProviderService from "./tax-provider"
import TotalsService from "./totals"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import StoreService from "./store"
import { SalesChannelService } from "./index"
type InjectedDependencies = {
manager: EntityManager
@@ -476,8 +476,27 @@ class CartService extends TransactionBaseService {
}
}
if (data.billing_address) {
if (!regCountries.includes(data.billing_address.country_code!)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Billing country not in region"
)
}
rawCart.billing_address = data.billing_address
}
if (data.billing_address_id) {
const addr = await addressRepo.findOne(data.billing_address_id)
if (addr?.country_code && !regCountries.includes(addr.country_code)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Billing country not in region"
)
}
rawCart.billing_address_id = data.billing_address_id
}
const remainingFields: (keyof Cart)[] = [
"billing_address_id",
"context",
"type",
"metadata",

View File

@@ -1,18 +1,18 @@
import { MedusaError } from "medusa-core-utils"
import { Brackets, EntityManager, FindManyOptions, UpdateResult } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import { Cart, CartType, DraftOrder, DraftOrderStatus } from "../models"
import { DraftOrderRepository } from "../repositories/draft-order"
import { PaymentRepository } from "../repositories/payment"
import EventBusService from "./event-bus"
import CartService from "./cart"
import LineItemService from "./line-item"
import { OrderRepository } from "../repositories/order"
import { PaymentRepository } from "../repositories/payment"
import { ExtendedFindConfig, FindConfig } from "../types/common"
import { DraftOrderCreateProps } from "../types/draft-orders"
import { buildQuery } from "../utils"
import CartService from "./cart"
import EventBusService from "./event-bus"
import LineItemService from "./line-item"
import ProductVariantService from "./product-variant"
import ShippingOptionService from "./shipping-option"
import { Cart, CartType, DraftOrder, DraftOrderStatus } from "../models"
import { AdminPostDraftOrdersReq } from "../api/routes/admin/draft-orders"
import { TransactionBaseService } from "../interfaces"
import { ExtendedFindConfig, FindConfig } from "../types/common"
import { buildQuery } from "../utils"
type InjectedDependencies = {
manager: EntityManager
@@ -59,6 +59,7 @@ class DraftOrderService extends TransactionBaseService {
productVariantService,
shippingOptionService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.manager_ = manager
@@ -232,7 +233,7 @@ class DraftOrderService extends TransactionBaseService {
* @param data - data to create draft order from
* @return the created draft order
*/
async create(data: AdminPostDraftOrdersReq): Promise<DraftOrder> {
async create(data: DraftOrderCreateProps): Promise<DraftOrder> {
return await this.atomicPhase_(
async (transactionManager: EntityManager) => {
const draftOrderRepo = transactionManager.getCustomRepository(

View File

@@ -1,18 +1,18 @@
export { default as AuthService } from "./auth"
export { default as BatchJobService } from "./batch-job"
export { default as CartService } from "./cart"
export { default as ClaimItemService } from "./claim-item"
export { default as ClaimService } from "./claim"
export { default as ClaimItemService } from "./claim-item"
export { default as CurrencyService } from "./currency"
export { default as CustomShippingOptionService } from "./custom-shipping-option"
export { default as CustomerGroupService } from "./customer-group"
export { default as CustomerService } from "./customer"
export { default as CustomerGroupService } from "./customer-group"
export { default as DiscountService } from "./discount"
export { default as DiscountConditionService } from "./discount-condition"
export { default as DraftOrderService } from "./draft-order"
export { default as EventBusService } from "./event-bus"
export { default as FulfillmentProviderService } from "./fulfillment-provider"
export { default as FulfillmentService } from "./fulfillment"
export { default as FulfillmentProviderService } from "./fulfillment-provider"
export { default as GiftCardService } from "./gift-card"
export { default as IdempotencyKeyService } from "./idempotency-key"
export { default as InventoryService } from "./inventory"
@@ -25,17 +25,17 @@ export { default as OauthService } from "./oauth"
export { default as OrderService } from "./order"
export { default as OrderEditService } from "./order-edit"
export { default as OrderEditItemChangeService } from "./order-edit-item-change"
export { default as PaymentProviderService } from "./payment-provider"
export { default as PaymentCollectionService } from "./payment-collection"
export { default as PricingService } from "./pricing"
export { default as PaymentProviderService } from "./payment-provider"
export { default as PriceListService } from "./price-list"
export { default as ProductCollectionService } from "./product-collection"
export { default as PricingService } from "./pricing"
export { default as ProductService } from "./product"
export { default as ProductCollectionService } from "./product-collection"
export { default as ProductTypeService } from "./product-type"
export { default as ProductVariantService } from "./product-variant"
export { default as RegionService } from "./region"
export { default as ReturnReasonService } from "./return-reason"
export { default as ReturnService } from "./return"
export { default as ReturnReasonService } from "./return-reason"
export { default as SalesChannelService } from "./sales-channel"
export { default as SearchService } from "./search"
export { default as ShippingOptionService } from "./shipping-option"

View File

@@ -1 +1,37 @@
import { AddressPayload } from "./common"
export type DraftOrderListSelector = { q?: string }
export type DraftOrderCreateProps = {
status?: string
email: string
billing_address_id?: string
billing_address?: Partial<AddressPayload>
shipping_address_id?: string
shipping_address?: Partial<AddressPayload>
items: Item[]
region_id: string
discounts?: Discount[]
customer_id?: string
no_notification_order?: boolean
shipping_methods: ShippingMethod[]
metadata?: Record<string, unknown>
}
type ShippingMethod = {
option_id: string
data?: Record<string, unknown>
price?: number
}
type Discount = {
code: string
}
type Item = {
title?: string
unit_price?: number
variant_id?: string
quantity: number
metadata?: Record<string, unknown>
}