fix(medusa): migrate cart service to typescript (#884)

* fix: migrate cart service to typescript

* fix: jsdoc inventory service

* fix: revert route unit test change

* fix: typo

* fix: revert integration test packages

* fix: cleanup

* fix: tests

* fix: integration tests

* fix: create props type guards

* fix: move total field to common types
This commit is contained in:
Sebastian Rindom
2021-12-07 19:26:00 +01:00
committed by GitHub
parent 03ffab5d61
commit ed04132533
17 changed files with 509 additions and 208 deletions

View File

@@ -95,7 +95,7 @@ Object {
"payment_authorized_at": null,
"payment_id": null,
"region_id": "test-region",
"shipping_address_id": "test-shipping-address",
"shipping_address_id": StringMatching /\\^addr_\\*/,
"type": "swap",
"updated_at": Any<String>,
},
@@ -266,7 +266,7 @@ Object {
"payment_authorized_at": null,
"payment_id": null,
"region_id": "test-region",
"shipping_address_id": "test-shipping-address",
"shipping_address_id": StringMatching /\\^addr_\\*/,
"type": "swap",
"updated_at": Any<String>,
},

View File

@@ -137,6 +137,7 @@ describe("/store/carts", () => {
type: "swap",
created_at: expect.any(String),
updated_at: expect.any(String),
shipping_address_id: expect.stringMatching(/^addr_*/),
metadata: {
swap_id: expect.stringMatching(/^swap_*/),
},
@@ -220,6 +221,7 @@ describe("/store/carts", () => {
cart: {
id: expect.stringMatching(/^cart_*/),
billing_address_id: "test-billing-address",
shipping_address_id: expect.stringMatching(/^addr_*/),
type: "swap",
created_at: expect.any(String),
updated_at: expect.any(String),

View File

@@ -10,6 +10,7 @@ export const MedusaErrorTypes = {
INVALID_DATA: "invalid_data",
NOT_FOUND: "not_found",
NOT_ALLOWED: "not_allowed",
UNEXPECTED_STATE: "unexpected_state",
}
export const MedusaErrorCodes = {

View File

@@ -1,5 +1,5 @@
import { Router } from "express"
import { DraftOrder, Order } from "../../../.."
import { DraftOrder, Order, Cart } from "../../../.."
import middlewares from "../../../middlewares"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
@@ -62,7 +62,7 @@ export const defaultAdminDraftOrdersCartRelations = [
"discounts.rule",
]
export const defaultAdminDraftOrdersCartFields = [
export const defaultAdminDraftOrdersCartFields: (keyof Cart)[] = [
"subtotal",
"tax_total",
"shipping_total",

View File

@@ -7,6 +7,7 @@ import {
defaultAdminDraftOrdersFields,
} from "."
import { DraftOrder } from "../../../.."
import { LineItemUpdate } from "../../../../types/cart"
import { CartService, DraftOrderService } from "../../../../services"
import { validator } from "../../../../utils/validator"
/**
@@ -112,15 +113,6 @@ export default async (req, res) => {
})
}
class LineItemUpdate {
title?: string
unit_price?: number
quantity?: number
metadata?: object = {}
region_id?: string
variant_id?: string
}
export class AdminPostDraftOrdersDraftOrderLineItemsItemReq {
@IsString()
@IsOptional()

View File

@@ -6,6 +6,8 @@ import {
SwapService,
} from "../../../../services"
import { Order } from "../../../../models/order"
/**
* @oas [post] /carts/{id}/complete
* summary: "Complete a Cart"
@@ -144,7 +146,7 @@ export default async (req, res) => {
relations: ["payment", "payment_sessions"],
})
let order
let order: Order
// If cart is part of swap, we register swap as complete
switch (cart.type) {
@@ -183,6 +185,15 @@ export default async (req, res) => {
}
// case "payment_link":
default: {
if (typeof cart.total === "undefined") {
return {
response_code: 500,
response_body: {
message: "Unexpected state",
},
}
}
if (!cart.payment && cart.total > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,

View File

@@ -10,9 +10,11 @@ import {
import { MedusaError } from "medusa-core-utils"
import reqIp from "request-ip"
import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService, LineItemService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { AddressPayload } from "../../../../types/common"
/**
* @oas [post] /carts
@@ -74,8 +76,11 @@ export default async (req, res) => {
await entityManager.transaction(async (manager) => {
// Add a default region if no region has been specified
let regionId = validated.region_id
if (!validated.region_id) {
let regionId: string
if (typeof validated.region_id !== "undefined") {
regionId = validated.region_id
} else {
const regionService = req.scope.resolve("regionService")
const regions = await regionService.withTransaction(manager).list({})
@@ -90,11 +95,11 @@ export default async (req, res) => {
}
const toCreate: {
region_id: string | undefined
region_id: string
context: object
customer_id?: string
email?: string
shipping_address?: object
shipping_address?: Partial<AddressPayload>
} = {
region_id: regionId,
context: {
@@ -117,11 +122,11 @@ export default async (req, res) => {
country_code: validated.country_code.toLowerCase(),
}
}
let cart = await cartService.withTransaction(manager).create(toCreate)
if (validated.items) {
await Promise.all(
validated.items.map(async i => {
validated.items.map(async (i) => {
const lineItem = await lineItemService
.withTransaction(manager)
.generate(i.variant_id, regionId, i.quantity)

View File

@@ -97,7 +97,7 @@ export default (app, container) => {
return app
}
export const defaultStoreCartFields = [
export const defaultStoreCartFields: (keyof Cart)[] = [
"subtotal",
"tax_total",
"shipping_total",

View File

@@ -9,6 +9,7 @@ import {
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { AddressPayload } from "../../../../types/common"
import { CartUpdateProps } from "../../../../types/cart"
import { IsType } from "../../../../utils/validators/is-type"
import { validator } from "../../../../utils/validator"
@@ -87,7 +88,25 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
// Update the cart
await cartService.update(id, validated)
const { shipping_address, billing_address, ...rest } = validated
const toUpdate: CartUpdateProps = {
...rest,
}
if (typeof shipping_address === "string") {
toUpdate.shipping_address_id = shipping_address
} else {
toUpdate.shipping_address = shipping_address
}
if (typeof billing_address === "string") {
toUpdate.billing_address_id = billing_address
} else {
toUpdate.billing_address = billing_address
}
await cartService.update(id, toUpdate)
// If the cart has payment sessions update these
const updated = await cartService.retrieve(id, {

View File

@@ -50,46 +50,46 @@ export class Address {
id: string
@Index()
@Column({ nullable: true })
customer_id: string
@Column({ type: "text", nullable: true })
customer_id: string | null
@ManyToOne(() => Customer)
@JoinColumn({ name: "customer_id" })
customer: Customer
customer: Customer | null
@Column({ nullable: true })
company: string
@Column({ type: "text", nullable: true })
company: string | null
@Column({ nullable: true })
first_name: string
@Column({ type: "text", nullable: true })
first_name: string | null
@Column({ nullable: true })
last_name: string
@Column({ type: "text", nullable: true })
last_name: string | null
@Column({ nullable: true })
address_1: string
@Column({ type: "text", nullable: true })
address_1: string | null
@Column({ nullable: true })
address_2: string
@Column({ type: "text", nullable: true })
address_2: string | null
@Column({ nullable: true })
city: string
@Column({ type: "text", nullable: true })
city: string | null
@Column({ nullable: true })
country_code: string
@Column({ type: "text", nullable: true })
country_code: string | null
@ManyToOne(() => Country)
@JoinColumn({ name: "country_code", referencedColumnName: "iso_2" })
country: Country
country: Country | null
@Column({ nullable: true })
province: string
@Column({ type: "text", nullable: true })
province: string | null
@Column({ nullable: true })
postal_code: string
@Column({ type: "text", nullable: true })
postal_code: string | null
@Column({ nullable: true })
phone: string
@Column({ type: "text", nullable: true })
phone: string | null
@CreateDateColumn({ type: resolveDbType("timestamptz") })
created_at: Date
@@ -98,7 +98,7 @@ export class Address {
updated_at: Date
@DeleteDateColumn({ type: resolveDbType("timestamptz") })
deleted_at: Date
deleted_at: Date | null
@DbAwareColumn({ type: "jsonb", nullable: true })
metadata: any

View File

@@ -145,7 +145,7 @@ export class Cart {
cascade: ["insert", "remove", "soft-remove"],
})
@JoinColumn({ name: "shipping_address_id" })
shipping_address: Address
shipping_address: Address | null
@OneToMany(() => LineItem, (lineItem) => lineItem.cart, {
cascade: ["insert", "remove"],
@@ -172,7 +172,7 @@ export class Cart {
referencedColumnName: "id",
},
})
discounts: Discount
discounts: Discount[]
@ManyToMany(() => GiftCard)
@JoinTable({
@@ -186,7 +186,7 @@ export class Cart {
referencedColumnName: "id",
},
})
gift_cards: GiftCard
gift_cards: GiftCard[]
@Index()
@Column({ nullable: true })
@@ -196,7 +196,7 @@ export class Cart {
@JoinColumn({ name: "customer_id" })
customer: Customer
payment_session: PaymentSession
payment_session: PaymentSession | null
@OneToMany(() => PaymentSession, (paymentSession) => paymentSession.cart, {
cascade: true,
@@ -217,7 +217,7 @@ export class Cart {
shipping_methods: ShippingMethod[]
@DbAwareColumn({ type: "enum", enum: CartType, default: "default" })
type: boolean
type: CartType
@Column({ type: resolveDbType("timestamptz"), nullable: true })
completed_at: Date
@@ -243,15 +243,14 @@ export class Cart {
@DbAwareColumn({ type: "jsonb", nullable: true })
context: any
// Total fields
shipping_total: number
discount_total: number
tax_total: number
refunded_total: number
total: number
subtotal: number
refundable_amount: number
gift_card_total: number
shipping_total?: number
discount_total?: number
tax_total?: number
refunded_total?: number
total?: number
subtotal?: number
refundable_amount?: number
gift_card_total?: number
@BeforeInsert()
private beforeInsert(): undefined | void {

View File

@@ -32,10 +32,7 @@ export class PaymentSession {
@Column()
cart_id: string
@ManyToOne(
() => Cart,
cart => cart.payment_sessions
)
@ManyToOne(() => Cart, (cart) => cart.payment_sessions)
@JoinColumn({ name: "cart_id" })
cart: Cart
@@ -43,8 +40,8 @@ export class PaymentSession {
@Column()
provider_id: string
@Column({ nullable: true })
is_selected: boolean
@Column({ type: "boolean", nullable: true })
is_selected: boolean | null
@DbAwareColumn({ type: "enum", enum: PaymentSessionStatus })
status: string

View File

@@ -1,7 +1,64 @@
import _ from "lodash"
import { EntityManager, DeepPartial } from "typeorm"
import { MedusaError, Validator } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { ShippingMethodRepository } from "../repositories/shipping-method"
import { CartRepository } from "../repositories/cart"
import { AddressRepository } from "../repositories/address"
import { PaymentSessionRepository } from "../repositories/payment-session"
import { Address } from "../models/address"
import { Discount } from "../models/discount"
import { Cart } from "../models/cart"
import { Customer } from "../models/customer"
import { LineItem } from "../models/line-item"
import { ShippingMethod } from "../models/shipping-method"
import { CustomShippingOption } from "../models/custom-shipping-option"
import { TotalField, FindConfig } from "../types/common"
import {
FilterableCartProps,
LineItemUpdate,
CartUpdateProps,
CartCreateProps,
} from "../types/cart"
import EventBusService from "./event-bus"
import ProductVariantService from "./product-variant"
import ProductService from "./product"
import RegionService from "./region"
import LineItemService from "./line-item"
import PaymentProviderService from "./payment-provider"
import ShippingOptionService from "./shipping-option"
import CustomerService from "./customer"
import DiscountService from "./discount"
import GiftCardService from "./gift-card"
import TotalsService from "./totals"
import InventoryService from "./inventory"
import CustomShippingOptionService from "./custom-shipping-option"
type CartConstructorProps = {
manager: EntityManager
cartRepository: typeof CartRepository
shippingMethodRepository: typeof ShippingMethodRepository
addressRepository: typeof AddressRepository
paymentSessionRepository: typeof PaymentSessionRepository
eventBusService: EventBusService
paymentProviderService: PaymentProviderService
productService: ProductService
productVariantService: ProductVariantService
regionService: RegionService
lineItemService: LineItemService
shippingOptionService: ShippingOptionService
customerService: CustomerService
discountService: DiscountService
giftCardService: GiftCardService
totalsService: TotalsService
inventoryService: InventoryService
customShippingOptionService: CustomShippingOptionService
}
/* Provides layer to manipulate carts.
* @implements BaseService
*/
@@ -12,6 +69,25 @@ class CartService extends BaseService {
UPDATED: "cart.updated",
}
private manager_: EntityManager
private shippingMethodRepository_: typeof ShippingMethodRepository
private cartRepository_: typeof CartRepository
private eventBus_: EventBusService
private productVariantService_: ProductVariantService
private productService_: ProductService
private regionService_: RegionService
private lineItemService_: LineItemService
private paymentProviderService_: PaymentProviderService
private customerService_: CustomerService
private shippingOptionService_: ShippingOptionService
private discountService_: DiscountService
private giftCardService_: GiftCardService
private totalsService_: TotalsService
private addressRepository_: typeof AddressRepository
private paymentSessionRepository_: typeof PaymentSessionRepository
private inventoryService_: InventoryService
private customShippingOptionService_: CustomShippingOptionService
constructor({
manager,
cartRepository,
@@ -31,7 +107,7 @@ class CartService extends BaseService {
paymentSessionRepository,
inventoryService,
customShippingOptionService,
}) {
}: CartConstructorProps) {
super()
/** @private @const {EntityManager} */
@@ -89,7 +165,7 @@ class CartService extends BaseService {
this.customShippingOptionService_ = customShippingOptionService
}
withTransaction(transactionManager) {
withTransaction(transactionManager: EntityManager): CartService {
if (!transactionManager) {
return this
}
@@ -139,7 +215,9 @@ class CartService extends BaseService {
* @typedef {LineItemContent[]} LineItemContentArray
*/
transformQueryForTotals_(config) {
transformQueryForTotals_(
config: FindConfig<Cart>
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
let { select, relations } = config
if (!select) {
@@ -159,7 +237,9 @@ class CartService extends BaseService {
"total",
]
const totalsToSelect = select.filter((v) => totalFields.includes(v))
const totalsToSelect = select.filter((v) =>
totalFields.includes(v)
) as TotalField[]
if (totalsToSelect.length > 0) {
const relationSet = new Set(relations)
relationSet.add("items")
@@ -184,26 +264,40 @@ class CartService extends BaseService {
}
}
async decorateTotals_(cart, totalsFields = []) {
if (totalsFields.includes("shipping_total")) {
cart.shipping_total = await this.totalsService_.getShippingTotal(cart)
async decorateTotals_(
cart: Cart,
totalsToSelect: TotalField[]
): Promise<Cart> {
const totals: { [K in TotalField]?: number } = {}
for (const key of totalsToSelect) {
switch (key) {
case "total": {
totals.total = await this.totalsService_.getTotal(cart)
break
}
case "shipping_total": {
totals.shipping_total = this.totalsService_.getShippingTotal(cart)
break
}
case "discount_total":
totals.discount_total = this.totalsService_.getDiscountTotal(cart)
break
case "tax_total":
totals.tax_total = await this.totalsService_.getTaxTotal(cart)
break
case "gift_card_total":
totals.gift_card_total = this.totalsService_.getGiftCardTotal(cart)
break
case "subtotal":
totals.subtotal = this.totalsService_.getSubtotal(cart)
break
default:
break
}
}
if (totalsFields.includes("discount_total")) {
cart.discount_total = await this.totalsService_.getDiscountTotal(cart)
}
if (totalsFields.includes("tax_total")) {
cart.tax_total = await this.totalsService_.getTaxTotal(cart)
}
if (totalsFields.includes("gift_card_total")) {
cart.gift_card_total = await this.totalsService_.getGiftCardTotal(cart)
}
if (totalsFields.includes("subtotal")) {
cart.subtotal = await this.totalsService_.getSubtotal(cart)
}
if (totalsFields.includes("total")) {
cart.total = await this.totalsService_.getTotal(cart)
}
return cart
return Object.assign(cart, totals)
}
/**
@@ -211,22 +305,14 @@ class CartService extends BaseService {
* @param {Object} config - config object
* @return {Promise} the result of the find operation
*/
list(selector, config = {}) {
async list(
selector: FilterableCartProps,
config: FindConfig<Cart> = {}
): Promise<Cart[]> {
const cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
const query = {
where: selector,
}
if (config.select) {
query.select = config.select
}
if (config.relations) {
query.relations = config.relations
}
return cartRepo.find(query)
const query = this.buildQuery_(selector, config)
return await cartRepo.find(query)
}
/**
@@ -235,16 +321,20 @@ class CartService extends BaseService {
* @param {Object} options - the options to get a cart
* @return {Promise<Cart>} the cart document.
*/
async retrieve(cartId, options = {}) {
async retrieve(
cartId: string,
options: FindConfig<Cart> = {}
): Promise<Cart> {
const cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals_(options)
const query = {
where: { id: validatedId },
}
const query = this.buildQuery_(
{ id: validatedId },
{ ...options, select, relations }
)
if (relations && relations.length > 0) {
query.relations = relations
@@ -252,10 +342,13 @@ class CartService extends BaseService {
if (select && select.length > 0) {
query.select = select
} else {
delete query.select
}
const rels = query.relations
delete query.relations
const raw = await cartRepo.findOneWithRelations(rels, query)
if (!raw) {
@@ -265,8 +358,7 @@ class CartService extends BaseService {
)
}
const cart = await this.decorateTotals_(raw, totalsToSelect)
return cart
return await this.decorateTotals_(raw, totalsToSelect)
}
/**
@@ -274,10 +366,11 @@ class CartService extends BaseService {
* @param {Object} data - the data to create the cart with
* @return {Promise} the result of the create operation
*/
async create(data) {
return this.atomicPhase_(async (manager) => {
async create(data: CartCreateProps): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const addressRepo = manager.getCustomRepository(this.addressRepository_)
const { region_id } = data
if (!region_id) {
throw new MedusaError(
@@ -289,26 +382,35 @@ class CartService extends BaseService {
const region = await this.regionService_.retrieve(region_id, {
relations: ["countries"],
})
const regCountries = region.countries.map(({ iso_2 }) => iso_2)
if (data.email) {
const toCreate: DeepPartial<Cart> = {}
toCreate.region_id = region.id
if (typeof data.email !== "undefined") {
const customer = await this.createOrFetchUserFromEmail_(data.email)
data.customer = customer
data.customer_id = customer.id
data.email = customer.email
toCreate.customer = customer
toCreate.customer_id = customer.id
toCreate.email = customer.email
}
if (data.shipping_address_id) {
if (typeof data.shipping_address_id !== "undefined") {
const addr = await addressRepo.findOne(data.shipping_address_id)
data.shipping_address = addr
if (addr && !regCountries.includes(addr.country_code)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Shipping country not in region"
)
}
toCreate.shipping_address = addr
}
if (!data.shipping_address) {
if (region.countries.length === 1) {
// Preselect the country if the region only has 1
// and create address entity
data.shipping_address = addressRepo.create({
toCreate.shipping_address = addressRepo.create({
country_code: regCountries[0],
})
}
@@ -319,14 +421,26 @@ class CartService extends BaseService {
"Shipping country not in region"
)
}
toCreate.shipping_address = data.shipping_address
}
const toCreate = {
...data,
region_id: region.id,
const remainingFields: (keyof Cart)[] = [
"billing_address_id",
"context",
"type",
"metadata",
"discounts",
"gift_cards",
]
for (const k of remainingFields) {
if (typeof data[k] !== "undefined") {
toCreate[k] = data[k]
}
}
const inProgress = await cartRepo.create(toCreate)
const inProgress = cartRepo.create(toCreate)
const result = await cartRepo.save(inProgress)
await this.eventBus_
.withTransaction(manager)
@@ -343,8 +457,8 @@ class CartService extends BaseService {
* @param {LineItem} lineItemId - the line item to remove.
* @return {Promise} the result of the update operation
*/
async removeLineItem(cartId, lineItemId) {
return this.atomicPhase_(async (manager) => {
async removeLineItem(cartId: string, lineItemId: string): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: [
"items",
@@ -395,7 +509,10 @@ class CartService extends BaseService {
* @param {LineItem} lineItem - the line item
* @return {boolean}
*/
validateLineItemShipping_(shippingMethods, lineItem) {
validateLineItemShipping_(
shippingMethods: ShippingMethod[],
lineItem: LineItem
): boolean {
if (!lineItem.variant_id) {
return true
}
@@ -422,8 +539,8 @@ class CartService extends BaseService {
* @param {LineItem} lineItem - the line item to add.
* @return {Promise} the result of the update operation
*/
async addLineItem(cartId, lineItem) {
return this.atomicPhase_(async (manager) => {
async addLineItem(cartId: string, lineItem: LineItem): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: [
"shipping_methods",
@@ -434,12 +551,13 @@ class CartService extends BaseService {
],
})
let currentItem
let currentItem: LineItem | undefined
if (lineItem.should_merge) {
currentItem = cart.items.find((line) => {
if (line.should_merge && line.variant_id === lineItem.variant_id) {
return _.isEqual(line.metadata, lineItem.metadata)
}
return false
})
}
@@ -501,8 +619,12 @@ class CartService extends BaseService {
* include an id field.
* @return {Promise} the result of the update operation
*/
async updateLineItem(cartId, lineItemId, lineItemUpdate) {
return this.atomicPhase_(async (manager) => {
async updateLineItem(
cartId: string,
lineItemId: string,
lineItemUpdate: LineItemUpdate
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: ["items", "payment_sessions"],
})
@@ -550,7 +672,7 @@ class CartService extends BaseService {
* @param {Cart} cart - the the cart to adjust free shipping for
* @param {boolean} shouldAdd - flag to indicate, if we should add or remove
*/
async adjustFreeShipping_(cart, shouldAdd) {
async adjustFreeShipping_(cart: Cart, shouldAdd: boolean): Promise<void> {
if (cart.shipping_methods?.length) {
// if any free shipping discounts, we ensure to update shipping method amount
if (shouldAdd) {
@@ -585,8 +707,8 @@ class CartService extends BaseService {
}
}
async update(cartId, update) {
return this.atomicPhase_(async (manager) => {
async update(cartId: string, update: CartUpdateProps): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const cart = await this.retrieve(cartId, {
select: [
@@ -613,16 +735,16 @@ class CartService extends BaseService {
],
})
if ("region_id" in update) {
if (typeof update.region_id !== "undefined") {
const countryCode =
update.country_code || update.shipping_address?.country_code
(update.country_code || update.shipping_address?.country_code) ?? null
await this.setRegion_(cart, update.region_id, countryCode)
}
if ("customer_id" in update) {
if (typeof update.customer_id !== "undefined") {
await this.updateCustomerId_(cart, update.customer_id)
} else {
if ("email" in update) {
if (typeof update.email !== "undefined") {
const customer = await this.createOrFetchUserFromEmail_(update.email)
cart.customer = customer
cart.customer_id = customer.id
@@ -632,16 +754,32 @@ class CartService extends BaseService {
const addrRepo = manager.getCustomRepository(this.addressRepository_)
if ("shipping_address_id" in update || "shipping_address" in update) {
const address = update.shipping_address_id || update.shipping_address
await this.updateShippingAddress_(cart, address, addrRepo)
let address: string | Partial<Address> | undefined
if (typeof update.shipping_address_id !== "undefined") {
address = update.shipping_address_id
} else if (typeof update.shipping_address !== "undefined") {
address = update.shipping_address
}
if (typeof address !== "undefined") {
await this.updateShippingAddress_(cart, address, addrRepo)
}
}
if ("billing_address_id" in update || "billing_address" in update) {
const address = update.billing_address_id || update.billing_address
await this.updateBillingAddress_(cart, address, addrRepo)
let address: string | Partial<Address> | undefined
if (typeof update.billing_address_id !== "undefined") {
address = update.billing_address_id
} else if (typeof update.billing_address !== "undefined") {
address = update.billing_address
}
if (typeof address !== "undefined") {
await this.updateBillingAddress_(cart, address, addrRepo)
}
}
if ("discounts" in update) {
if (typeof update.discounts !== "undefined") {
const previousDiscounts = cart.discounts
cart.discounts = []
@@ -670,7 +808,7 @@ class CartService extends BaseService {
if ("gift_cards" in update) {
cart.gift_cards = []
for (const { code } of update.gift_cards) {
for (const { code } of update.gift_cards!) {
await this.applyGiftCard_(cart, code)
}
}
@@ -688,11 +826,11 @@ class CartService extends BaseService {
}
if ("completed_at" in update) {
cart.completed_at = update.completed_at
cart.completed_at = update.completed_at!
}
if ("payment_authorized_at" in update) {
cart.payment_authorized_at = update.payment_authorized_at
cart.payment_authorized_at = update.payment_authorized_at!
}
const result = await cartRepo.save(cart)
@@ -717,7 +855,7 @@ class CartService extends BaseService {
* @param {string} customerId - the customer to add to cart
* @return {Promise} the result of the update operation
*/
async updateCustomerId_(cart, customerId) {
async updateCustomerId_(cart: Cart, customerId: string): Promise<void> {
const customer = await this.customerService_
.withTransaction(this.transactionManager_)
.retrieve(customerId)
@@ -732,7 +870,7 @@ class CartService extends BaseService {
* @param {string} email - the email to use
* @return {Promise} the resultign customer object
*/
async createOrFetchUserFromEmail_(email) {
async createOrFetchUserFromEmail_(email: string): Promise<Customer> {
const schema = Validator.string().email().required()
const { value, error } = schema.validate(email.toLowerCase())
if (error) {
@@ -765,17 +903,24 @@ class CartService extends BaseService {
* updates
* @return {Promise} the result of the update operation
*/
async updateBillingAddress_(cart, addressOrId, addrRepo) {
async updateBillingAddress_(
cart: Cart,
addressOrId: Partial<Address> | string,
addrRepo: AddressRepository
): Promise<void> {
let address: Address
if (typeof addressOrId === `string`) {
addressOrId = await addrRepo.findOne({
address = (await addrRepo.findOne({
where: { id: addressOrId },
})
})) as Address
} else {
address = addressOrId as Address
}
addressOrId.country_code = addressOrId.country_code.toLowerCase()
address.country_code = address.country_code?.toLowerCase() ?? null
if (addressOrId.id) {
const updated = await addrRepo.save(addressOrId)
if (address.id) {
const updated = await addrRepo.save(address)
cart.billing_address = updated
} else {
if (cart.billing_address_id) {
@@ -783,10 +928,10 @@ class CartService extends BaseService {
where: { id: cart.billing_address_id },
})
await addrRepo.save({ ...addr, ...addressOrId })
await addrRepo.save({ ...addr, ...address })
} else {
const created = addrRepo.create({
...addressOrId,
...address,
})
cart.billing_address = created
@@ -803,25 +948,31 @@ class CartService extends BaseService {
* updates
* @return {Promise} the result of the update operation
*/
async updateShippingAddress_(cart, addressOrId, addrRepo) {
async updateShippingAddress_(
cart: Cart,
addressOrId: Partial<Address> | string,
addrRepo: AddressRepository
): Promise<void> {
let address: Address
if (addressOrId === null) {
cart.shipping_address = null
return
}
if (typeof addressOrId === `string`) {
addressOrId = await addrRepo.findOne({
address = (await addrRepo.findOne({
where: { id: addressOrId },
})
})) as Address
} else {
address = addressOrId as Address
}
addressOrId.country_code = addressOrId.country_code?.toLowerCase() ?? null
address.country_code = address.country_code?.toLowerCase() ?? null
if (
addressOrId.country_code &&
!cart.region.countries.find(
({ iso_2 }) => addressOrId.country_code === iso_2
)
address.country_code &&
!cart.region.countries.find(({ iso_2 }) => address.country_code === iso_2)
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -829,8 +980,8 @@ class CartService extends BaseService {
)
}
if (addressOrId.id) {
const updated = await addrRepo.save(addressOrId)
if (address.id) {
const updated = await addrRepo.save(address)
cart.shipping_address = updated
} else {
if (cart.shipping_address_id) {
@@ -838,10 +989,10 @@ class CartService extends BaseService {
where: { id: cart.shipping_address_id },
})
await addrRepo.save({ ...addr, ...addressOrId })
await addrRepo.save({ ...addr, ...address })
} else {
const created = addrRepo.create({
...addressOrId,
...address,
})
cart.shipping_address = created
@@ -849,7 +1000,7 @@ class CartService extends BaseService {
}
}
async applyGiftCard_(cart, code) {
async applyGiftCard_(cart: Cart, code: string): Promise<void> {
const giftCard = await this.giftCardService_.retrieveByCode(code)
if (giftCard.is_disabled) {
@@ -883,7 +1034,7 @@ class CartService extends BaseService {
* @param {string} discountCode - the discount code
* @return {Promise} the result of the update operation
*/
async applyDiscount(cart, discountCode) {
async applyDiscount(cart: Cart, discountCode: string): Promise<void> {
const discount = await this.discountService_.retrieveByCode(discountCode, [
"rule",
"rule.valid_for",
@@ -982,8 +1133,8 @@ class CartService extends BaseService {
* @param {string} discountCode - the discount code to remove
* @return {Promise<Cart>} the resulting cart
*/
async removeDiscount(cartId, discountCode) {
return this.atomicPhase_(async (manager) => {
async removeDiscount(cartId: string, discountCode: string): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: [
"discounts",
@@ -1030,8 +1181,8 @@ class CartService extends BaseService {
* @param {string} cartId - the id of the cart to update the payment session for
* @param {object} update - the data to update the payment session with
*/
async updatePaymentSession(cartId, update) {
return this.atomicPhase_(async (manager) => {
async updatePaymentSession(cartId: string, update: object): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"],
})
@@ -1064,8 +1215,8 @@ class CartService extends BaseService {
* this could be IP address or similar for fraud handling.
* @return {Promise<Cart>} the resulting cart
*/
async authorizePayment(cartId, context = {}) {
return this.atomicPhase_(async (manager) => {
async authorizePayment(cartId: string, context: object = {}): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepository = manager.getCustomRepository(this.cartRepository_)
const cart = await this.retrieve(cartId, {
@@ -1073,6 +1224,13 @@ class CartService extends BaseService {
relations: ["region", "payment_sessions"],
})
if (typeof cart.total === "undefined") {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"cart.total should be defined"
)
}
// If cart total is 0, we don't perform anything payment related
if (cart.total <= 0) {
cart.payment_authorized_at = new Date()
@@ -1111,8 +1269,8 @@ class CartService extends BaseService {
* @param {string} providerId - the id of the provider to be set to the cart
* @return {Promise} result of update operation
*/
async setPaymentSession(cartId, providerId) {
return this.atomicPhase_(async (manager) => {
async setPaymentSession(cartId: string, providerId: string): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const psRepo = manager.getCustomRepository(this.paymentSessionRepository_)
const cart = await this.retrieve(cartId, {
@@ -1141,8 +1299,8 @@ class CartService extends BaseService {
}
await Promise.all(
cart.payment_sessions.map((ps) => {
return psRepo.save({ ...ps, is_selected: null })
cart.payment_sessions.map(async (ps) => {
return await psRepo.save({ ...ps, is_selected: null })
})
)
@@ -1150,6 +1308,13 @@ class CartService extends BaseService {
(ps) => ps.provider_id === providerId
)
if (!sess) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"Could not find payment session"
)
}
sess.is_selected = true
await psRepo.save(sess)
@@ -1173,12 +1338,13 @@ class CartService extends BaseService {
* session for
* @return {Promise} the result of the update operation.
*/
async setPaymentSessions(cartOrCartId) {
return this.atomicPhase_(async (manager) => {
async setPaymentSessions(cartOrCartId: Cart | string): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const psRepo = manager.getCustomRepository(this.paymentSessionRepository_)
const cartId =
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
const cart = await this.retrieve(cartId, {
select: [
"gift_card_total",
@@ -1205,8 +1371,15 @@ class CartService extends BaseService {
const region = cart.region
if (typeof cart.total === "undefined") {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"cart.total should be defined"
)
}
// If there are existing payment sessions ensure that these are up to date
const seen = []
const seen: string[] = []
if (cart.payment_sessions && cart.payment_sessions.length) {
for (const session of cart.payment_sessions) {
if (
@@ -1258,8 +1431,11 @@ class CartService extends BaseService {
* should be removed.
* @return {Promise<Cart>} the resulting cart.
*/
async deletePaymentSession(cartId, providerId) {
return this.atomicPhase_(async (manager) => {
async deletePaymentSession(
cartId: string,
providerId: string
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"],
})
@@ -1299,8 +1475,11 @@ class CartService extends BaseService {
* should be removed.
* @return {Promise<Cart>} the resulting cart.
*/
async refreshPaymentSession(cartId, providerId) {
return this.atomicPhase_(async (manager) => {
async refreshPaymentSession(
cartId: string,
providerId: string
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"],
})
@@ -1338,8 +1517,12 @@ class CartService extends BaseService {
* @param {Object} data - the fulmillment data for the method
* @return {Promise} the result of the update operation
*/
async addShippingMethod(cartId, optionId, data) {
return this.atomicPhase_(async (manager) => {
async addShippingMethod(
cartId: string,
optionId: string,
data: object = {}
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
select: ["subtotal"],
relations: [
@@ -1431,7 +1614,10 @@ class CartService extends BaseService {
* @param {string} optionId - id of the normal or custom shipping option to find in the cartCustomShippingOptions
* @return {CustomShippingOption | undefined}
*/
findCustomShippingOption(cartCustomShippingOptions, optionId) {
findCustomShippingOption(
cartCustomShippingOptions: CustomShippingOption[],
optionId: string
): CustomShippingOption | undefined {
const customOption = cartCustomShippingOptions?.find(
(cso) => cso.shipping_option_id === optionId
)
@@ -1454,7 +1640,11 @@ class CartService extends BaseService {
* @param {string} countryCode - the country code to set the country to
* @return {Promise} the result of the update operation
*/
async setRegion_(cart, regionId, countryCode) {
async setRegion_(
cart: Cart,
regionId: string,
countryCode: string | null
): Promise<void> {
if (cart.completed_at || cart.payment_authorized_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
@@ -1506,18 +1696,18 @@ class CartService extends BaseService {
* First check if there is an existing shipping address on the cart if so
* fetch the entire thing so we can modify the shipping country
*/
let shippingAddress = {}
let shippingAddress: Partial<Address> = {}
if (cart.shipping_address_id) {
shippingAddress = await addrRepo.findOne({
shippingAddress = (await addrRepo.findOne({
where: { id: cart.shipping_address_id },
})
})) as Address
}
/*
* If the client has specified which country code we are updating to check
* that that country is in fact in the country and perform the update.
*/
if (countryCode !== undefined) {
if (countryCode !== null) {
if (
!region.countries.find(
({ iso_2 }) => iso_2 === countryCode.toLowerCase()
@@ -1580,9 +1770,10 @@ class CartService extends BaseService {
if (d.regions.find(({ id }) => id === regionId)) {
return d
}
return null
})
cart.discounts = newDiscounts.filter((d) => !!d)
cart.discounts = newDiscounts.filter(Boolean) as Discount[]
}
cart.gift_cards = []
@@ -1603,11 +1794,11 @@ class CartService extends BaseService {
/**
* Deletes a cart from the database. Completed carts cannot be deleted.
* @param {string} cartId - the id of the cart to delete
* @return {Promise<Cart?>} the deleted cart or undefined if the cart was
* @return {Promise<string>} the deleted cart or undefined if the cart was
* not found.
*/
async delete(cartId) {
return this.atomicPhase_(async (manager) => {
async delete(cartId: string): Promise<string> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: [
"items",
@@ -1633,7 +1824,7 @@ class CartService extends BaseService {
}
const cartRepo = manager.getCustomRepository(this.cartRepository_)
return cartRepo.remove(cartId)
return cartRepo.remove(cart)
})
}
@@ -1646,8 +1837,12 @@ class CartService extends BaseService {
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
async setMetadata(cartId, key, value) {
return this.atomicPhase_(async (manager) => {
async setMetadata(
cartId: string,
key: string,
value: string | number
): Promise<Cart> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId)
@@ -1658,7 +1853,7 @@ class CartService extends BaseService {
)
}
const cart = await cartRepo.findOne(validatedId)
const cart = (await cartRepo.findOne(validatedId)) as Cart
const existing = cart.metadata || {}
cart.metadata = {
@@ -1680,8 +1875,8 @@ class CartService extends BaseService {
* @param {string} key - key for metadata field
* @return {Promise} resolves to the updated result.
*/
async deleteMetadata(cartId, key) {
return this.atomicPhase_(async (manager) => {
async deleteMetadata(cartId: string, key: string): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId)

View File

@@ -57,7 +57,7 @@ class InventoryService extends BaseService {
* allows backorders or if the inventory quantity is greater than `quantity`.
* @param {string} variantId - the id of the variant to check
* @param {number} quantity - the number of units to check availability for
* @return {boolean} true if the inventory covers the quantity
* @return {Promise<boolean>} true if the inventory covers the quantity
*/
async confirmInventory(variantId, quantity) {
// if variantId is undefined then confirm inventory as it

View File

@@ -201,7 +201,7 @@ class ShippingOptionService extends BaseService {
/**
* Removes a given shipping method
* @param {string} sm - the shipping method to remove
* @param {ShippingMethod} sm - the shipping method to remove
*/
async deleteShippingMethod(sm) {
return this.atomicPhase_(async (manager) => {

View File

@@ -0,0 +1,70 @@
import { ValidateNested } from "class-validator"
import { IsType } from "../utils/validators/is-type"
import { CartType } from "../models/cart"
import {
AddressPayload,
DateComparisonOperator,
StringComparisonOperator,
} from "./common"
export class FilterableCartProps {
@ValidateNested()
@IsType([String, [String], StringComparisonOperator])
id?: string | string[] | StringComparisonOperator
@IsType([DateComparisonOperator])
created_at?: DateComparisonOperator
@IsType([DateComparisonOperator])
updated_at?: DateComparisonOperator
}
// TODO: Probably worth moving to `./line-item` instead
export type LineItemUpdate = {
title?: string
unit_price?: number
quantity?: number
metadata?: object
region_id?: string
variant_id?: string
}
class GiftCard {
code: string
}
class Discount {
code: string
}
export type CartCreateProps = {
region_id: string
email?: string
billing_address_id?: string
billing_address?: Partial<AddressPayload>
shipping_address_id?: string
shipping_address?: Partial<AddressPayload>
gift_cards?: GiftCard[]
discounts?: Discount[]
customer_id?: string
type?: CartType
context?: object
metadata?: object
}
export type CartUpdateProps = {
region_id?: string
country_code?: string
email?: string
shipping_address_id?: string
billing_address_id?: string
billing_address?: AddressPayload
shipping_address?: AddressPayload
completed_at?: Date
payment_authorized_at?: Date
gift_cards?: GiftCard[]
discounts?: Discount[]
customer_id?: string
context?: object
metadata?: object
}

View File

@@ -8,6 +8,16 @@ export type PartialPick<T, K extends keyof T> = {
[P in K]?: T[P]
}
export type TotalField =
| "shipping_total"
| "discount_total"
| "tax_total"
| "refunded_total"
| "total"
| "subtotal"
| "refundable_amount"
| "gift_card_total"
export interface FindConfig<Entity> {
select?: (keyof Entity)[]
skip?: number