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 authored and GitHub committed 2021-12-07 19:26:00 +01:00
1 parent 03ffab5d61
commit ed04132533
17 files changed
+508 -207

No files matched your search

@@ -95,7 +95,7 @@ Object {
"payment_authorized_at": null, "payment_authorized_at": null,
"payment_id": null, "payment_id": null,
"region_id": "test-region", "region_id": "test-region",
"shipping_address_id": "test-shipping-address", "shipping_address_id": StringMatching /\\^addr_\\*/,
"type": "swap", "type": "swap",
"updated_at": Any<String>, "updated_at": Any<String>,
}, },
@@ -266,7 +266,7 @@ Object {
"payment_authorized_at": null, "payment_authorized_at": null,
"payment_id": null, "payment_id": null,
"region_id": "test-region", "region_id": "test-region",
"shipping_address_id": "test-shipping-address", "shipping_address_id": StringMatching /\\^addr_\\*/,
"type": "swap", "type": "swap",
"updated_at": Any<String>, "updated_at": Any<String>,
}, },
@@ -137,6 +137,7 @@ describe("/store/carts", () => {
type: "swap", type: "swap",
created_at: expect.any(String), created_at: expect.any(String),
updated_at: expect.any(String), updated_at: expect.any(String),
shipping_address_id: expect.stringMatching(/^addr_*/),
metadata: { metadata: {
swap_id: expect.stringMatching(/^swap_*/), swap_id: expect.stringMatching(/^swap_*/),
}, },
@@ -220,6 +221,7 @@ describe("/store/carts", () => {
cart: { cart: {
id: expect.stringMatching(/^cart_*/), id: expect.stringMatching(/^cart_*/),
billing_address_id: "test-billing-address", billing_address_id: "test-billing-address",
shipping_address_id: expect.stringMatching(/^addr_*/),
type: "swap", type: "swap",
created_at: expect.any(String), created_at: expect.any(String),
updated_at: expect.any(String), updated_at: expect.any(String),
+1
View File
@@ -10,6 +10,7 @@ export const MedusaErrorTypes = {
INVALID_DATA: "invalid_data", INVALID_DATA: "invalid_data",
NOT_FOUND: "not_found", NOT_FOUND: "not_found",
NOT_ALLOWED: "not_allowed", NOT_ALLOWED: "not_allowed",
UNEXPECTED_STATE: "unexpected_state",
} }
export const MedusaErrorCodes = { export const MedusaErrorCodes = {
@@ -1,5 +1,5 @@
import { Router } from "express" import { Router } from "express"
import { DraftOrder, Order } from "../../../.." import { DraftOrder, Order, Cart } from "../../../.."
import middlewares from "../../../middlewares" import middlewares from "../../../middlewares"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common" import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
@@ -62,7 +62,7 @@ export const defaultAdminDraftOrdersCartRelations = [
"discounts.rule", "discounts.rule",
] ]
export const defaultAdminDraftOrdersCartFields = [ export const defaultAdminDraftOrdersCartFields: (keyof Cart)[] = [
"subtotal", "subtotal",
"tax_total", "tax_total",
"shipping_total", "shipping_total",
@@ -7,6 +7,7 @@ import {
defaultAdminDraftOrdersFields, defaultAdminDraftOrdersFields,
} from "." } from "."
import { DraftOrder } from "../../../.." import { DraftOrder } from "../../../.."
import { LineItemUpdate } from "../../../../types/cart"
import { CartService, DraftOrderService } from "../../../../services" import { CartService, DraftOrderService } from "../../../../services"
import { validator } from "../../../../utils/validator" 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 { export class AdminPostDraftOrdersDraftOrderLineItemsItemReq {
@IsString() @IsString()
@IsOptional() @IsOptional()
@@ -6,6 +6,8 @@ import {
SwapService, SwapService,
} from "../../../../services" } from "../../../../services"
import { Order } from "../../../../models/order"
/** /**
* @oas [post] /carts/{id}/complete * @oas [post] /carts/{id}/complete
* summary: "Complete a Cart" * summary: "Complete a Cart"
@@ -144,7 +146,7 @@ export default async (req, res) => {
relations: ["payment", "payment_sessions"], relations: ["payment", "payment_sessions"],
}) })
let order let order: Order
// If cart is part of swap, we register swap as complete // If cart is part of swap, we register swap as complete
switch (cart.type) { switch (cart.type) {
@@ -183,6 +185,15 @@ export default async (req, res) => {
} }
// case "payment_link": // case "payment_link":
default: { default: {
if (typeof cart.total === "undefined") {
return {
response_code: 500,
response_body: {
message: "Unexpected state",
},
}
}
if (!cart.payment && cart.total > 0) { if (!cart.payment && cart.total > 0) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -10,9 +10,11 @@ import {
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import reqIp from "request-ip" import reqIp from "request-ip"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "." import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService, LineItemService } from "../../../../services" import { CartService, LineItemService } from "../../../../services"
import { validator } from "../../../../utils/validator" import { validator } from "../../../../utils/validator"
import { AddressPayload } from "../../../../types/common"
/** /**
* @oas [post] /carts * @oas [post] /carts
@@ -74,8 +76,11 @@ export default async (req, res) => {
await entityManager.transaction(async (manager) => { await entityManager.transaction(async (manager) => {
// Add a default region if no region has been specified // Add a default region if no region has been specified
let regionId = validated.region_id let regionId: string
if (!validated.region_id) {
if (typeof validated.region_id !== "undefined") {
regionId = validated.region_id
} else {
const regionService = req.scope.resolve("regionService") const regionService = req.scope.resolve("regionService")
const regions = await regionService.withTransaction(manager).list({}) const regions = await regionService.withTransaction(manager).list({})
@@ -90,11 +95,11 @@ export default async (req, res) => {
} }
const toCreate: { const toCreate: {
region_id: string | undefined region_id: string
context: object context: object
customer_id?: string customer_id?: string
email?: string email?: string
shipping_address?: object shipping_address?: Partial<AddressPayload>
} = { } = {
region_id: regionId, region_id: regionId,
context: { context: {
@@ -121,7 +126,7 @@ export default async (req, res) => {
let cart = await cartService.withTransaction(manager).create(toCreate) let cart = await cartService.withTransaction(manager).create(toCreate)
if (validated.items) { if (validated.items) {
await Promise.all( await Promise.all(
validated.items.map(async i => { validated.items.map(async (i) => {
const lineItem = await lineItemService const lineItem = await lineItemService
.withTransaction(manager) .withTransaction(manager)
.generate(i.variant_id, regionId, i.quantity) .generate(i.variant_id, regionId, i.quantity)
@@ -97,7 +97,7 @@ export default (app, container) => {
return app return app
} }
export const defaultStoreCartFields = [ export const defaultStoreCartFields: (keyof Cart)[] = [
"subtotal", "subtotal",
"tax_total", "tax_total",
"shipping_total", "shipping_total",
@@ -9,6 +9,7 @@ import {
import { defaultStoreCartFields, defaultStoreCartRelations } from "." import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services" import { CartService } from "../../../../services"
import { AddressPayload } from "../../../../types/common" import { AddressPayload } from "../../../../types/common"
import { CartUpdateProps } from "../../../../types/cart"
import { IsType } from "../../../../utils/validators/is-type" import { IsType } from "../../../../utils/validators/is-type"
import { validator } from "../../../../utils/validator" import { validator } from "../../../../utils/validator"
@@ -87,7 +88,25 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService") const cartService: CartService = req.scope.resolve("cartService")
// Update the cart // 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 // If the cart has payment sessions update these
const updated = await cartService.retrieve(id, { const updated = await cartService.retrieve(id, {
+25 -25
View File
@@ -50,46 +50,46 @@ export class Address {
id: string id: string
@Index() @Index()
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
customer_id: string customer_id: string | null
@ManyToOne(() => Customer) @ManyToOne(() => Customer)
@JoinColumn({ name: "customer_id" }) @JoinColumn({ name: "customer_id" })
customer: Customer customer: Customer | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
company: string company: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
first_name: string first_name: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
last_name: string last_name: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
address_1: string address_1: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
address_2: string address_2: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
city: string city: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
country_code: string country_code: string | null
@ManyToOne(() => Country) @ManyToOne(() => Country)
@JoinColumn({ name: "country_code", referencedColumnName: "iso_2" }) @JoinColumn({ name: "country_code", referencedColumnName: "iso_2" })
country: Country country: Country | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
province: string province: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
postal_code: string postal_code: string | null
@Column({ nullable: true }) @Column({ type: "text", nullable: true })
phone: string phone: string | null
@CreateDateColumn({ type: resolveDbType("timestamptz") }) @CreateDateColumn({ type: resolveDbType("timestamptz") })
created_at: Date created_at: Date
@@ -98,7 +98,7 @@ export class Address {
updated_at: Date updated_at: Date
@DeleteDateColumn({ type: resolveDbType("timestamptz") }) @DeleteDateColumn({ type: resolveDbType("timestamptz") })
deleted_at: Date deleted_at: Date | null
@DbAwareColumn({ type: "jsonb", nullable: true }) @DbAwareColumn({ type: "jsonb", nullable: true })
metadata: any metadata: any
+13 -14
View File
@@ -145,7 +145,7 @@ export class Cart {
cascade: ["insert", "remove", "soft-remove"], cascade: ["insert", "remove", "soft-remove"],
}) })
@JoinColumn({ name: "shipping_address_id" }) @JoinColumn({ name: "shipping_address_id" })
shipping_address: Address shipping_address: Address | null
@OneToMany(() => LineItem, (lineItem) => lineItem.cart, { @OneToMany(() => LineItem, (lineItem) => lineItem.cart, {
cascade: ["insert", "remove"], cascade: ["insert", "remove"],
@@ -172,7 +172,7 @@ export class Cart {
referencedColumnName: "id", referencedColumnName: "id",
}, },
}) })
discounts: Discount discounts: Discount[]
@ManyToMany(() => GiftCard) @ManyToMany(() => GiftCard)
@JoinTable({ @JoinTable({
@@ -186,7 +186,7 @@ export class Cart {
referencedColumnName: "id", referencedColumnName: "id",
}, },
}) })
gift_cards: GiftCard gift_cards: GiftCard[]
@Index() @Index()
@Column({ nullable: true }) @Column({ nullable: true })
@@ -196,7 +196,7 @@ export class Cart {
@JoinColumn({ name: "customer_id" }) @JoinColumn({ name: "customer_id" })
customer: Customer customer: Customer
payment_session: PaymentSession payment_session: PaymentSession | null
@OneToMany(() => PaymentSession, (paymentSession) => paymentSession.cart, { @OneToMany(() => PaymentSession, (paymentSession) => paymentSession.cart, {
cascade: true, cascade: true,
@@ -217,7 +217,7 @@ export class Cart {
shipping_methods: ShippingMethod[] shipping_methods: ShippingMethod[]
@DbAwareColumn({ type: "enum", enum: CartType, default: "default" }) @DbAwareColumn({ type: "enum", enum: CartType, default: "default" })
type: boolean type: CartType
@Column({ type: resolveDbType("timestamptz"), nullable: true }) @Column({ type: resolveDbType("timestamptz"), nullable: true })
completed_at: Date completed_at: Date
@@ -243,15 +243,14 @@ export class Cart {
@DbAwareColumn({ type: "jsonb", nullable: true }) @DbAwareColumn({ type: "jsonb", nullable: true })
context: any context: any
// Total fields shipping_total?: number
shipping_total: number discount_total?: number
discount_total: number tax_total?: number
tax_total: number refunded_total?: number
refunded_total: number total?: number
total: number subtotal?: number
subtotal: number refundable_amount?: number
refundable_amount: number gift_card_total?: number
gift_card_total: number
@BeforeInsert() @BeforeInsert()
private beforeInsert(): undefined | void { private beforeInsert(): undefined | void {
@@ -32,10 +32,7 @@ export class PaymentSession {
@Column() @Column()
cart_id: string cart_id: string
@ManyToOne( @ManyToOne(() => Cart, (cart) => cart.payment_sessions)
() => Cart,
cart => cart.payment_sessions
)
@JoinColumn({ name: "cart_id" }) @JoinColumn({ name: "cart_id" })
cart: Cart cart: Cart
@@ -43,8 +40,8 @@ export class PaymentSession {
@Column() @Column()
provider_id: string provider_id: string
@Column({ nullable: true }) @Column({ type: "boolean", nullable: true })
is_selected: boolean is_selected: boolean | null
@DbAwareColumn({ type: "enum", enum: PaymentSessionStatus }) @DbAwareColumn({ type: "enum", enum: PaymentSessionStatus })
status: string status: string
@@ -1,7 +1,64 @@
import _ from "lodash" import _ from "lodash"
import { EntityManager, DeepPartial } from "typeorm"
import { MedusaError, Validator } from "medusa-core-utils" import { MedusaError, Validator } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" 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. /* Provides layer to manipulate carts.
* @implements BaseService * @implements BaseService
*/ */
@@ -12,6 +69,25 @@ class CartService extends BaseService {
UPDATED: "cart.updated", 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({ constructor({
manager, manager,
cartRepository, cartRepository,
@@ -31,7 +107,7 @@ class CartService extends BaseService {
paymentSessionRepository, paymentSessionRepository,
inventoryService, inventoryService,
customShippingOptionService, customShippingOptionService,
}) { }: CartConstructorProps) {
super() super()
/** @private @const {EntityManager} */ /** @private @const {EntityManager} */
@@ -89,7 +165,7 @@ class CartService extends BaseService {
this.customShippingOptionService_ = customShippingOptionService this.customShippingOptionService_ = customShippingOptionService
} }
withTransaction(transactionManager) { withTransaction(transactionManager: EntityManager): CartService {
if (!transactionManager) { if (!transactionManager) {
return this return this
} }
@@ -139,7 +215,9 @@ class CartService extends BaseService {
* @typedef {LineItemContent[]} LineItemContentArray * @typedef {LineItemContent[]} LineItemContentArray
*/ */
transformQueryForTotals_(config) { transformQueryForTotals_(
config: FindConfig<Cart>
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
let { select, relations } = config let { select, relations } = config
if (!select) { if (!select) {
@@ -159,7 +237,9 @@ class CartService extends BaseService {
"total", "total",
] ]
const totalsToSelect = select.filter((v) => totalFields.includes(v)) const totalsToSelect = select.filter((v) =>
totalFields.includes(v)
) as TotalField[]
if (totalsToSelect.length > 0) { if (totalsToSelect.length > 0) {
const relationSet = new Set(relations) const relationSet = new Set(relations)
relationSet.add("items") relationSet.add("items")
@@ -184,26 +264,40 @@ class CartService extends BaseService {
} }
} }
async decorateTotals_(cart, totalsFields = []) { async decorateTotals_(
if (totalsFields.includes("shipping_total")) { cart: Cart,
cart.shipping_total = await this.totalsService_.getShippingTotal(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) return Object.assign(cart, totals)
}
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
} }
/** /**
@@ -211,22 +305,14 @@ class CartService extends BaseService {
* @param {Object} config - config object * @param {Object} config - config object
* @return {Promise} the result of the find operation * @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 cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
const query = { const query = this.buildQuery_(selector, config)
where: selector, return await cartRepo.find(query)
}
if (config.select) {
query.select = config.select
}
if (config.relations) {
query.relations = config.relations
}
return cartRepo.find(query)
} }
/** /**
@@ -235,16 +321,20 @@ class CartService extends BaseService {
* @param {Object} options - the options to get a cart * @param {Object} options - the options to get a cart
* @return {Promise<Cart>} the cart document. * @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 cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId) const validatedId = this.validateId_(cartId)
const { select, relations, totalsToSelect } = const { select, relations, totalsToSelect } =
this.transformQueryForTotals_(options) this.transformQueryForTotals_(options)
const query = { const query = this.buildQuery_(
where: { id: validatedId }, { id: validatedId },
} { ...options, select, relations }
)
if (relations && relations.length > 0) { if (relations && relations.length > 0) {
query.relations = relations query.relations = relations
@@ -252,10 +342,13 @@ class CartService extends BaseService {
if (select && select.length > 0) { if (select && select.length > 0) {
query.select = select query.select = select
} else {
delete query.select
} }
const rels = query.relations const rels = query.relations
delete query.relations delete query.relations
const raw = await cartRepo.findOneWithRelations(rels, query) const raw = await cartRepo.findOneWithRelations(rels, query)
if (!raw) { if (!raw) {
@@ -265,8 +358,7 @@ class CartService extends BaseService {
) )
} }
const cart = await this.decorateTotals_(raw, totalsToSelect) return await this.decorateTotals_(raw, totalsToSelect)
return cart
} }
/** /**
@@ -274,10 +366,11 @@ class CartService extends BaseService {
* @param {Object} data - the data to create the cart with * @param {Object} data - the data to create the cart with
* @return {Promise} the result of the create operation * @return {Promise} the result of the create operation
*/ */
async create(data) { async create(data: CartCreateProps): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_) const cartRepo = manager.getCustomRepository(this.cartRepository_)
const addressRepo = manager.getCustomRepository(this.addressRepository_) const addressRepo = manager.getCustomRepository(this.addressRepository_)
const { region_id } = data const { region_id } = data
if (!region_id) { if (!region_id) {
throw new MedusaError( throw new MedusaError(
@@ -289,26 +382,35 @@ class CartService extends BaseService {
const region = await this.regionService_.retrieve(region_id, { const region = await this.regionService_.retrieve(region_id, {
relations: ["countries"], relations: ["countries"],
}) })
const regCountries = region.countries.map(({ iso_2 }) => iso_2) 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) const customer = await this.createOrFetchUserFromEmail_(data.email)
data.customer = customer toCreate.customer = customer
data.customer_id = customer.id toCreate.customer_id = customer.id
data.email = customer.email 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) 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 (!data.shipping_address) {
if (region.countries.length === 1) { if (region.countries.length === 1) {
// Preselect the country if the region only has 1 // Preselect the country if the region only has 1
// and create address entity // and create address entity
data.shipping_address = addressRepo.create({ toCreate.shipping_address = addressRepo.create({
country_code: regCountries[0], country_code: regCountries[0],
}) })
} }
@@ -319,14 +421,26 @@ class CartService extends BaseService {
"Shipping country not in region" "Shipping country not in region"
) )
} }
toCreate.shipping_address = data.shipping_address
} }
const toCreate = { const remainingFields: (keyof Cart)[] = [
...data, "billing_address_id",
region_id: region.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) const result = await cartRepo.save(inProgress)
await this.eventBus_ await this.eventBus_
.withTransaction(manager) .withTransaction(manager)
@@ -343,8 +457,8 @@ class CartService extends BaseService {
* @param {LineItem} lineItemId - the line item to remove. * @param {LineItem} lineItemId - the line item to remove.
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async removeLineItem(cartId, lineItemId) { async removeLineItem(cartId: string, lineItemId: string): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: [
"items", "items",
@@ -395,7 +509,10 @@ class CartService extends BaseService {
* @param {LineItem} lineItem - the line item * @param {LineItem} lineItem - the line item
* @return {boolean} * @return {boolean}
*/ */
validateLineItemShipping_(shippingMethods, lineItem) { validateLineItemShipping_(
shippingMethods: ShippingMethod[],
lineItem: LineItem
): boolean {
if (!lineItem.variant_id) { if (!lineItem.variant_id) {
return true return true
} }
@@ -422,8 +539,8 @@ class CartService extends BaseService {
* @param {LineItem} lineItem - the line item to add. * @param {LineItem} lineItem - the line item to add.
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async addLineItem(cartId, lineItem) { async addLineItem(cartId: string, lineItem: LineItem): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: [
"shipping_methods", "shipping_methods",
@@ -434,12 +551,13 @@ class CartService extends BaseService {
], ],
}) })
let currentItem let currentItem: LineItem | undefined
if (lineItem.should_merge) { if (lineItem.should_merge) {
currentItem = cart.items.find((line) => { currentItem = cart.items.find((line) => {
if (line.should_merge && line.variant_id === lineItem.variant_id) { if (line.should_merge && line.variant_id === lineItem.variant_id) {
return _.isEqual(line.metadata, lineItem.metadata) return _.isEqual(line.metadata, lineItem.metadata)
} }
return false
}) })
} }
@@ -501,8 +619,12 @@ class CartService extends BaseService {
* include an id field. * include an id field.
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async updateLineItem(cartId, lineItemId, lineItemUpdate) { async updateLineItem(
return this.atomicPhase_(async (manager) => { cartId: string,
lineItemId: string,
lineItemUpdate: LineItemUpdate
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: ["items", "payment_sessions"], relations: ["items", "payment_sessions"],
}) })
@@ -550,7 +672,7 @@ class CartService extends BaseService {
* @param {Cart} cart - the the cart to adjust free shipping for * @param {Cart} cart - the the cart to adjust free shipping for
* @param {boolean} shouldAdd - flag to indicate, if we should add or remove * @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 (cart.shipping_methods?.length) {
// if any free shipping discounts, we ensure to update shipping method amount // if any free shipping discounts, we ensure to update shipping method amount
if (shouldAdd) { if (shouldAdd) {
@@ -585,8 +707,8 @@ class CartService extends BaseService {
} }
} }
async update(cartId, update) { async update(cartId: string, update: CartUpdateProps): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_) const cartRepo = manager.getCustomRepository(this.cartRepository_)
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
select: [ select: [
@@ -613,16 +735,16 @@ class CartService extends BaseService {
], ],
}) })
if ("region_id" in update) { if (typeof update.region_id !== "undefined") {
const countryCode = 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) 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) await this.updateCustomerId_(cart, update.customer_id)
} else { } else {
if ("email" in update) { if (typeof update.email !== "undefined") {
const customer = await this.createOrFetchUserFromEmail_(update.email) const customer = await this.createOrFetchUserFromEmail_(update.email)
cart.customer = customer cart.customer = customer
cart.customer_id = customer.id cart.customer_id = customer.id
@@ -632,16 +754,32 @@ class CartService extends BaseService {
const addrRepo = manager.getCustomRepository(this.addressRepository_) const addrRepo = manager.getCustomRepository(this.addressRepository_)
if ("shipping_address_id" in update || "shipping_address" in update) { if ("shipping_address_id" in update || "shipping_address" in update) {
const address = update.shipping_address_id || update.shipping_address let address: string | Partial<Address> | undefined
await this.updateShippingAddress_(cart, address, addrRepo) 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) { if ("billing_address_id" in update || "billing_address" in update) {
const address = update.billing_address_id || update.billing_address let address: string | Partial<Address> | undefined
await this.updateBillingAddress_(cart, address, addrRepo) 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 const previousDiscounts = cart.discounts
cart.discounts = [] cart.discounts = []
@@ -670,7 +808,7 @@ class CartService extends BaseService {
if ("gift_cards" in update) { if ("gift_cards" in update) {
cart.gift_cards = [] cart.gift_cards = []
for (const { code } of update.gift_cards) { for (const { code } of update.gift_cards!) {
await this.applyGiftCard_(cart, code) await this.applyGiftCard_(cart, code)
} }
} }
@@ -688,11 +826,11 @@ class CartService extends BaseService {
} }
if ("completed_at" in update) { if ("completed_at" in update) {
cart.completed_at = update.completed_at cart.completed_at = update.completed_at!
} }
if ("payment_authorized_at" in update) { 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) const result = await cartRepo.save(cart)
@@ -717,7 +855,7 @@ class CartService extends BaseService {
* @param {string} customerId - the customer to add to cart * @param {string} customerId - the customer to add to cart
* @return {Promise} the result of the update operation * @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_ const customer = await this.customerService_
.withTransaction(this.transactionManager_) .withTransaction(this.transactionManager_)
.retrieve(customerId) .retrieve(customerId)
@@ -732,7 +870,7 @@ class CartService extends BaseService {
* @param {string} email - the email to use * @param {string} email - the email to use
* @return {Promise} the resultign customer object * @return {Promise} the resultign customer object
*/ */
async createOrFetchUserFromEmail_(email) { async createOrFetchUserFromEmail_(email: string): Promise<Customer> {
const schema = Validator.string().email().required() const schema = Validator.string().email().required()
const { value, error } = schema.validate(email.toLowerCase()) const { value, error } = schema.validate(email.toLowerCase())
if (error) { if (error) {
@@ -765,17 +903,24 @@ class CartService extends BaseService {
* updates * updates
* @return {Promise} the result of the update operation * @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`) { if (typeof addressOrId === `string`) {
addressOrId = await addrRepo.findOne({ address = (await addrRepo.findOne({
where: { id: addressOrId }, 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) { if (address.id) {
const updated = await addrRepo.save(addressOrId) const updated = await addrRepo.save(address)
cart.billing_address = updated cart.billing_address = updated
} else { } else {
if (cart.billing_address_id) { if (cart.billing_address_id) {
@@ -783,10 +928,10 @@ class CartService extends BaseService {
where: { id: cart.billing_address_id }, where: { id: cart.billing_address_id },
}) })
await addrRepo.save({ ...addr, ...addressOrId }) await addrRepo.save({ ...addr, ...address })
} else { } else {
const created = addrRepo.create({ const created = addrRepo.create({
...addressOrId, ...address,
}) })
cart.billing_address = created cart.billing_address = created
@@ -803,25 +948,31 @@ class CartService extends BaseService {
* updates * updates
* @return {Promise} the result of the update operation * @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) { if (addressOrId === null) {
cart.shipping_address = null cart.shipping_address = null
return return
} }
if (typeof addressOrId === `string`) { if (typeof addressOrId === `string`) {
addressOrId = await addrRepo.findOne({ address = (await addrRepo.findOne({
where: { id: addressOrId }, 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 ( if (
addressOrId.country_code && address.country_code &&
!cart.region.countries.find( !cart.region.countries.find(({ iso_2 }) => address.country_code === iso_2)
({ iso_2 }) => addressOrId.country_code === iso_2
)
) { ) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -829,8 +980,8 @@ class CartService extends BaseService {
) )
} }
if (addressOrId.id) { if (address.id) {
const updated = await addrRepo.save(addressOrId) const updated = await addrRepo.save(address)
cart.shipping_address = updated cart.shipping_address = updated
} else { } else {
if (cart.shipping_address_id) { if (cart.shipping_address_id) {
@@ -838,10 +989,10 @@ class CartService extends BaseService {
where: { id: cart.shipping_address_id }, where: { id: cart.shipping_address_id },
}) })
await addrRepo.save({ ...addr, ...addressOrId }) await addrRepo.save({ ...addr, ...address })
} else { } else {
const created = addrRepo.create({ const created = addrRepo.create({
...addressOrId, ...address,
}) })
cart.shipping_address = created 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) const giftCard = await this.giftCardService_.retrieveByCode(code)
if (giftCard.is_disabled) { if (giftCard.is_disabled) {
@@ -883,7 +1034,7 @@ class CartService extends BaseService {
* @param {string} discountCode - the discount code * @param {string} discountCode - the discount code
* @return {Promise} the result of the update operation * @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, [ const discount = await this.discountService_.retrieveByCode(discountCode, [
"rule", "rule",
"rule.valid_for", "rule.valid_for",
@@ -982,8 +1133,8 @@ class CartService extends BaseService {
* @param {string} discountCode - the discount code to remove * @param {string} discountCode - the discount code to remove
* @return {Promise<Cart>} the resulting cart * @return {Promise<Cart>} the resulting cart
*/ */
async removeDiscount(cartId, discountCode) { async removeDiscount(cartId: string, discountCode: string): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: [
"discounts", "discounts",
@@ -1030,8 +1181,8 @@ class CartService extends BaseService {
* @param {string} cartId - the id of the cart to update the payment session for * @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 * @param {object} update - the data to update the payment session with
*/ */
async updatePaymentSession(cartId, update) { async updatePaymentSession(cartId: string, update: object): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"], relations: ["payment_sessions"],
}) })
@@ -1064,8 +1215,8 @@ class CartService extends BaseService {
* this could be IP address or similar for fraud handling. * this could be IP address or similar for fraud handling.
* @return {Promise<Cart>} the resulting cart * @return {Promise<Cart>} the resulting cart
*/ */
async authorizePayment(cartId, context = {}) { async authorizePayment(cartId: string, context: object = {}): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepository = manager.getCustomRepository(this.cartRepository_) const cartRepository = manager.getCustomRepository(this.cartRepository_)
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
@@ -1073,6 +1224,13 @@ class CartService extends BaseService {
relations: ["region", "payment_sessions"], 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 is 0, we don't perform anything payment related
if (cart.total <= 0) { if (cart.total <= 0) {
cart.payment_authorized_at = new Date() 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 * @param {string} providerId - the id of the provider to be set to the cart
* @return {Promise} result of update operation * @return {Promise} result of update operation
*/ */
async setPaymentSession(cartId, providerId) { async setPaymentSession(cartId: string, providerId: string): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const psRepo = manager.getCustomRepository(this.paymentSessionRepository_) const psRepo = manager.getCustomRepository(this.paymentSessionRepository_)
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
@@ -1141,8 +1299,8 @@ class CartService extends BaseService {
} }
await Promise.all( await Promise.all(
cart.payment_sessions.map((ps) => { cart.payment_sessions.map(async (ps) => {
return psRepo.save({ ...ps, is_selected: null }) return await psRepo.save({ ...ps, is_selected: null })
}) })
) )
@@ -1150,6 +1308,13 @@ class CartService extends BaseService {
(ps) => ps.provider_id === providerId (ps) => ps.provider_id === providerId
) )
if (!sess) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"Could not find payment session"
)
}
sess.is_selected = true sess.is_selected = true
await psRepo.save(sess) await psRepo.save(sess)
@@ -1173,12 +1338,13 @@ class CartService extends BaseService {
* session for * session for
* @return {Promise} the result of the update operation. * @return {Promise} the result of the update operation.
*/ */
async setPaymentSessions(cartOrCartId) { async setPaymentSessions(cartOrCartId: Cart | string): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const psRepo = manager.getCustomRepository(this.paymentSessionRepository_) const psRepo = manager.getCustomRepository(this.paymentSessionRepository_)
const cartId = const cartId =
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
select: [ select: [
"gift_card_total", "gift_card_total",
@@ -1205,8 +1371,15 @@ class CartService extends BaseService {
const region = cart.region 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 // 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) { if (cart.payment_sessions && cart.payment_sessions.length) {
for (const session of cart.payment_sessions) { for (const session of cart.payment_sessions) {
if ( if (
@@ -1258,8 +1431,11 @@ class CartService extends BaseService {
* should be removed. * should be removed.
* @return {Promise<Cart>} the resulting cart. * @return {Promise<Cart>} the resulting cart.
*/ */
async deletePaymentSession(cartId, providerId) { async deletePaymentSession(
return this.atomicPhase_(async (manager) => { cartId: string,
providerId: string
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"], relations: ["payment_sessions"],
}) })
@@ -1299,8 +1475,11 @@ class CartService extends BaseService {
* should be removed. * should be removed.
* @return {Promise<Cart>} the resulting cart. * @return {Promise<Cart>} the resulting cart.
*/ */
async refreshPaymentSession(cartId, providerId) { async refreshPaymentSession(
return this.atomicPhase_(async (manager) => { cartId: string,
providerId: string
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"], relations: ["payment_sessions"],
}) })
@@ -1338,8 +1517,12 @@ class CartService extends BaseService {
* @param {Object} data - the fulmillment data for the method * @param {Object} data - the fulmillment data for the method
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async addShippingMethod(cartId, optionId, data) { async addShippingMethod(
return this.atomicPhase_(async (manager) => { cartId: string,
optionId: string,
data: object = {}
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
select: ["subtotal"], select: ["subtotal"],
relations: [ 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 * @param {string} optionId - id of the normal or custom shipping option to find in the cartCustomShippingOptions
* @return {CustomShippingOption | undefined} * @return {CustomShippingOption | undefined}
*/ */
findCustomShippingOption(cartCustomShippingOptions, optionId) { findCustomShippingOption(
cartCustomShippingOptions: CustomShippingOption[],
optionId: string
): CustomShippingOption | undefined {
const customOption = cartCustomShippingOptions?.find( const customOption = cartCustomShippingOptions?.find(
(cso) => cso.shipping_option_id === optionId (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 * @param {string} countryCode - the country code to set the country to
* @return {Promise} the result of the update operation * @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) { if (cart.completed_at || cart.payment_authorized_at) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_ALLOWED, 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 * 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 * fetch the entire thing so we can modify the shipping country
*/ */
let shippingAddress = {} let shippingAddress: Partial<Address> = {}
if (cart.shipping_address_id) { if (cart.shipping_address_id) {
shippingAddress = await addrRepo.findOne({ shippingAddress = (await addrRepo.findOne({
where: { id: cart.shipping_address_id }, where: { id: cart.shipping_address_id },
}) })) as Address
} }
/* /*
* If the client has specified which country code we are updating to check * 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. * that that country is in fact in the country and perform the update.
*/ */
if (countryCode !== undefined) { if (countryCode !== null) {
if ( if (
!region.countries.find( !region.countries.find(
({ iso_2 }) => iso_2 === countryCode.toLowerCase() ({ iso_2 }) => iso_2 === countryCode.toLowerCase()
@@ -1580,9 +1770,10 @@ class CartService extends BaseService {
if (d.regions.find(({ id }) => id === regionId)) { if (d.regions.find(({ id }) => id === regionId)) {
return d return d
} }
return null
}) })
cart.discounts = newDiscounts.filter((d) => !!d) cart.discounts = newDiscounts.filter(Boolean) as Discount[]
} }
cart.gift_cards = [] cart.gift_cards = []
@@ -1603,11 +1794,11 @@ class CartService extends BaseService {
/** /**
* Deletes a cart from the database. Completed carts cannot be deleted. * Deletes a cart from the database. Completed carts cannot be deleted.
* @param {string} cartId - the id of the cart to delete * @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. * not found.
*/ */
async delete(cartId) { async delete(cartId: string): Promise<string> {
return this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: [
"items", "items",
@@ -1633,7 +1824,7 @@ class CartService extends BaseService {
} }
const cartRepo = manager.getCustomRepository(this.cartRepository_) 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. * @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result. * @return {Promise} resolves to the updated result.
*/ */
async setMetadata(cartId, key, value) { async setMetadata(
return this.atomicPhase_(async (manager) => { cartId: string,
key: string,
value: string | number
): Promise<Cart> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_) const cartRepo = manager.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId) 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 || {} const existing = cart.metadata || {}
cart.metadata = { cart.metadata = {
@@ -1680,8 +1875,8 @@ class CartService extends BaseService {
* @param {string} key - key for metadata field * @param {string} key - key for metadata field
* @return {Promise} resolves to the updated result. * @return {Promise} resolves to the updated result.
*/ */
async deleteMetadata(cartId, key) { async deleteMetadata(cartId: string, key: string): Promise<Cart> {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager: EntityManager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_) const cartRepo = manager.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId) const validatedId = this.validateId_(cartId)
+1 -1
View File
@@ -57,7 +57,7 @@ class InventoryService extends BaseService {
* allows backorders or if the inventory quantity is greater than `quantity`. * allows backorders or if the inventory quantity is greater than `quantity`.
* @param {string} variantId - the id of the variant to check * @param {string} variantId - the id of the variant to check
* @param {number} quantity - the number of units to check availability for * @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) { async confirmInventory(variantId, quantity) {
// if variantId is undefined then confirm inventory as it // if variantId is undefined then confirm inventory as it
@@ -201,7 +201,7 @@ class ShippingOptionService extends BaseService {
/** /**
* Removes a given shipping method * Removes a given shipping method
* @param {string} sm - the shipping method to remove * @param {ShippingMethod} sm - the shipping method to remove
*/ */
async deleteShippingMethod(sm) { async deleteShippingMethod(sm) {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager) => {
+70
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
}
+10
View File
@@ -8,6 +8,16 @@ export type PartialPick<T, K extends keyof T> = {
[P in K]?: T[P] [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> { export interface FindConfig<Entity> {
select?: (keyof Entity)[] select?: (keyof Entity)[]
skip?: number skip?: number