diff --git a/integration-tests/api/__tests__/admin/draft-order.js b/integration-tests/api/__tests__/admin/draft-order.js index d70b7de1a2..612a8c269e 100644 --- a/integration-tests/api/__tests__/admin/draft-order.js +++ b/integration-tests/api/__tests__/admin/draft-order.js @@ -75,7 +75,7 @@ describe("/admin/draft-orders", () => { const payload = { email: "oli@test.dk", - shipping_address_id: "oli-shipping", + shipping_address: "oli-shipping", items: [ { variant_id: "test-variant", @@ -109,7 +109,7 @@ describe("/admin/draft-orders", () => { const payload = { email: "oli@test.dk", - shipping_address_id: "oli-shipping", + shipping_address: "oli-shipping", items: [ { variant_id: "test-variant", @@ -148,7 +148,7 @@ describe("/admin/draft-orders", () => { const payload = { email: "oli@test.dk", - shipping_address_id: "oli-shipping", + shipping_address: "oli-shipping", discounts: [{ code: "TEST" }], items: [ { @@ -302,6 +302,7 @@ describe("/admin/draft-orders", () => { ); // expect draft order to be complete expect(updatedDraftOrder.data.draft_order.status).toEqual("completed"); + expect(updatedDraftOrder.data.draft_order.completed_at).not.toEqual(null); }); }); describe("GET /admin/draft-orders", () => { diff --git a/integration-tests/api/__tests__/admin/order.js b/integration-tests/api/__tests__/admin/order.js index d4c7b27793..704dc277a5 100644 --- a/integration-tests/api/__tests__/admin/order.js +++ b/integration-tests/api/__tests__/admin/order.js @@ -67,7 +67,7 @@ describe("/admin/orders", () => { await manager.query(`DELETE FROM "user"`); }); - it("creates a cart", async () => { + it("gets orders", async () => { const api = useApi(); const response = await api diff --git a/integration-tests/api/__tests__/store/cart.js b/integration-tests/api/__tests__/store/cart.js index 6973014f02..9b5ceeb22b 100644 --- a/integration-tests/api/__tests__/store/cart.js +++ b/integration-tests/api/__tests__/store/cart.js @@ -20,6 +20,7 @@ describe("/store/carts", () => { await manager.query(`DELETE FROM "shipping_method"`); await manager.query(`DELETE FROM "shipping_option"`); await manager.query(`DELETE FROM "cart"`); + await manager.query(`DELETE FROM "address"`); await manager.query(`DELETE FROM "customer"`); await manager.query( `UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'` @@ -143,6 +144,41 @@ describe("/store/carts", () => { expect(response.status).toEqual(200); }); + it("updates address using string id", async () => { + const api = useApi(); + + const response = await api.post("/store/carts/test-cart", { + billing_address: "test-general-address", + shipping_address: "test-general-address", + }); + + expect(response.data.cart.shipping_address_id).toEqual( + "test-general-address" + ); + expect(response.data.cart.billing_address_id).toEqual( + "test-general-address" + ); + expect(response.status).toEqual(200); + }); + + it("updates address", async () => { + const api = useApi(); + + const response = await api.post("/store/carts/test-cart", { + shipping_address: { + first_name: "clark", + last_name: "kent", + address_1: "5th avenue", + city: "nyc", + country_code: "us", + postal_code: "something", + }, + }); + + expect(response.data.cart.shipping_address.first_name).toEqual("clark"); + expect(response.status).toEqual(200); + }); + it("adds free shipping to cart then removes it again", async () => { const api = useApi(); diff --git a/integration-tests/api/__tests__/store/draft-order.js b/integration-tests/api/__tests__/store/draft-order.js index 1389244436..2246f192b2 100644 --- a/integration-tests/api/__tests__/store/draft-order.js +++ b/integration-tests/api/__tests__/store/draft-order.js @@ -45,7 +45,9 @@ describe("/store/carts (draft-orders)", () => { await manager.query(`DELETE FROM "product"`); await manager.query(`DELETE FROM "shipping_method"`); await manager.query(`DELETE FROM "shipping_option"`); + await manager.query(`UPDATE "discount" SET rule_id=NULL`); await manager.query(`DELETE FROM "discount"`); + await manager.query(`DELETE FROM "discount_rule"`); await manager.query(`DELETE FROM "payment_provider"`); await manager.query(`DELETE FROM "payment_session"`); await manager.query(`UPDATE "payment" SET order_id=NULL`); @@ -64,6 +66,7 @@ describe("/store/carts (draft-orders)", () => { `UPDATE "country" SET region_id=NULL WHERE iso_2 = 'de'` ); await manager.query(`DELETE FROM "region"`); + await manager.query(`DELETE FROM "user"`); }); it("completes a cart for a draft order thereby creating an order for the draft order", async () => { diff --git a/integration-tests/api/helpers/cart-seeder.js b/integration-tests/api/helpers/cart-seeder.js index 7280e6d9bf..dfbf4d4ffe 100644 --- a/integration-tests/api/helpers/cart-seeder.js +++ b/integration-tests/api/helpers/cart-seeder.js @@ -7,6 +7,7 @@ const { ShippingProfile, ShippingOption, ShippingMethod, + Address, } = require("@medusajs/medusa"); module.exports = async (connection, data = {}) => { @@ -16,6 +17,12 @@ module.exports = async (connection, data = {}) => { type: "default", }); + await manager.insert(Address, { + id: "test-general-address", + first_name: "superman", + country_code: "us", + }); + const r = manager.create(Region, { id: "test-region", name: "Test Region", diff --git a/integration-tests/api/helpers/draft-order-seeder.js b/integration-tests/api/helpers/draft-order-seeder.js index 89b5aafbb7..b5d694daa0 100644 --- a/integration-tests/api/helpers/draft-order-seeder.js +++ b/integration-tests/api/helpers/draft-order-seeder.js @@ -12,6 +12,7 @@ const { DraftOrder, Discount, DiscountRule, + Payment, } = require("@medusajs/medusa"); module.exports = async (connection, data = {}) => { @@ -189,6 +190,19 @@ module.exports = async (connection, data = {}) => { metadata: { draft_order_id: "test-draft-order" }, }); + const pay = manager.create(Payment, { + id: "test-payment", + amount: 10000, + currency_code: "usd", + amount_refunded: 0, + provider_id: "test-pay", + data: {}, + }); + + await manager.save(pay); + + c.payment = pay; + await manager.save(c); await manager.insert(PaymentSession, { @@ -197,12 +211,12 @@ module.exports = async (connection, data = {}) => { provider_id: "test-pay", is_selected: true, data: {}, - status: "pending", + status: "authorized", }); const draftOrder = manager.create(DraftOrder, { id: "test-draft-order", - status: "awaiting", + status: "open", display_id: 4, cart_id: "test-cart", customer_id: "oli-test", diff --git a/packages/medusa-core-utils/src/validator.js b/packages/medusa-core-utils/src/validator.js index 4661f75578..43fe68f5cd 100644 --- a/packages/medusa-core-utils/src/validator.js +++ b/packages/medusa-core-utils/src/validator.js @@ -2,19 +2,29 @@ import Joi from "joi" Joi.objectId = require("joi-objectid")(Joi) +// if address is a string, we assume that it is an id Joi.address = () => { - return Joi.object().keys({ - first_name: Joi.string().required(), - last_name: Joi.string().required(), - address_1: Joi.string().required(), - address_2: Joi.string().allow(null), - city: Joi.string().required(), - country_code: Joi.string().required(), - province: Joi.string().allow(null), - postal_code: Joi.string().required(), - phone: Joi.string().optional(), - metadata: Joi.object().allow(null), - }) + return Joi.alternatives().try( + Joi.string(), + Joi.object().keys({ + first_name: Joi.string().required(), + last_name: Joi.string().required(), + address_1: Joi.string().required(), + address_2: Joi.string() + .allow(null, "") + .optional(), + city: Joi.string().required(), + country_code: Joi.string().required(), + province: Joi.string() + .allow(null, "") + .optional(), + postal_code: Joi.string().required(), + phone: Joi.string().optional(), + metadata: Joi.object() + .allow(null, {}) + .optional(), + }) + ) } Joi.dateFilter = () => { diff --git a/packages/medusa/src/api/routes/admin/draft-orders/create-draft-order.js b/packages/medusa/src/api/routes/admin/draft-orders/create-draft-order.js index 6263ebafe2..3c0e358a9e 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/create-draft-order.js +++ b/packages/medusa/src/api/routes/admin/draft-orders/create-draft-order.js @@ -25,12 +25,6 @@ import { defaultFields, defaultRelations } from "." * description: "The Address to be used for shipping." * anyOf: * - $ref: "#/components/schemas/address" - * billing_address_id: - * description: The id of an existing billing Address - * type: string - * shipping_address_id: - * description: The id of an existing shipping Address - * type: string * items: * description: The Line Items that have been received. * type: array @@ -98,15 +92,13 @@ import { defaultFields, defaultRelations } from "." export default async (req, res) => { const schema = Validator.object().keys({ status: Validator.string() - .valid("open", "awaiting", "completed") + .valid("open", "completed") .optional(), email: Validator.string() .email() .required(), billing_address: Validator.address().optional(), shipping_address: Validator.address().optional(), - billing_address_id: Validator.string().optional(), - shipping_address_id: Validator.string().optional(), items: Validator.array() .items({ variant_id: Validator.string() diff --git a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.js b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.js index efcac4cfa7..39b558d53f 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.js +++ b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.js @@ -66,10 +66,7 @@ export default async (req, res) => { .withTransaction(manager) .retrieve(id, { select: defaultFields, relations: ["cart"] }) - if ( - draftOrder.status === "completed" || - draftOrder.status === "awaiting" - ) { + if (draftOrder.status === "completed") { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "You are only allowed to update open draft orders" diff --git a/packages/medusa/src/api/routes/admin/draft-orders/delete-line-item.js b/packages/medusa/src/api/routes/admin/draft-orders/delete-line-item.js index e1eeaf95bf..05680fa190 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/delete-line-item.js +++ b/packages/medusa/src/api/routes/admin/draft-orders/delete-line-item.js @@ -35,10 +35,7 @@ export default async (req, res) => { .withTransaction(manager) .retrieve(id, { select: defaultFields }) - if ( - draftOrder.status === "completed" || - draftOrder.status === "awaiting" - ) { + if (draftOrder.status === "completed") { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "You are only allowed to update open draft orders" diff --git a/packages/medusa/src/api/routes/admin/draft-orders/update-draft-order.js b/packages/medusa/src/api/routes/admin/draft-orders/update-draft-order.js index 7c53814d25..6230f553d5 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/update-draft-order.js +++ b/packages/medusa/src/api/routes/admin/draft-orders/update-draft-order.js @@ -67,11 +67,6 @@ export default async (req, res) => { code: Validator.string(), }) .optional(), - gift_cards: Validator.array() - .items({ - code: Validator.string(), - }) - .optional(), customer_id: Validator.string().optional(), }) @@ -86,7 +81,7 @@ export default async (req, res) => { const draftOrder = await draftOrderService.retrieve(id) - if (draftOrder.status === "completed" || draftOrder.status === "awaiting") { + if (draftOrder.status === "completed") { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "You are only allowed to update open draft orders" diff --git a/packages/medusa/src/api/routes/admin/draft-orders/update-line-item.js b/packages/medusa/src/api/routes/admin/draft-orders/update-line-item.js index 56a2350d3e..c1171f228e 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/update-line-item.js +++ b/packages/medusa/src/api/routes/admin/draft-orders/update-line-item.js @@ -64,10 +64,7 @@ export default async (req, res) => { relations: ["cart", "cart.items"], }) - if ( - draftOrder.status === "completed" || - draftOrder.status === "awaiting" - ) { + if (draftOrder.status === "completed") { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "You are only allowed to update open draft orders" diff --git a/packages/medusa/src/api/routes/store/carts/update-cart.js b/packages/medusa/src/api/routes/store/carts/update-cart.js index dbd871c676..83f1e79301 100644 --- a/packages/medusa/src/api/routes/store/carts/update-cart.js +++ b/packages/medusa/src/api/routes/store/carts/update-cart.js @@ -75,8 +75,8 @@ export default async (req, res) => { email: Validator.string() .email() .optional(), - billing_address: Validator.object().optional(), - shipping_address: Validator.object().optional(), + billing_address: Validator.address().optional(), + shipping_address: Validator.address().optional(), gift_cards: Validator.array() .items({ code: Validator.string(), diff --git a/packages/medusa/src/migrations/1623063141210-draft_order_completed_at.ts b/packages/medusa/src/migrations/1623063141210-draft_order_completed_at.ts new file mode 100644 index 0000000000..787e160241 --- /dev/null +++ b/packages/medusa/src/migrations/1623063141210-draft_order_completed_at.ts @@ -0,0 +1,28 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class draftOrderCompletedAt1623063141210 implements MigrationInterface { + name = 'draftOrderCompletedAt1623063141210' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "draft_order" ADD "completed_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TYPE "public"."draft_order_status_enum" RENAME TO "draft_order_status_enum_old"`); + await queryRunner.query(`CREATE TYPE "draft_order_status_enum" AS ENUM('open', 'completed')`); + await queryRunner.query(`ALTER TABLE "draft_order" ALTER COLUMN "status" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "draft_order" ALTER COLUMN "status" TYPE "draft_order_status_enum" USING "status"::"text"::"draft_order_status_enum"`); + await queryRunner.query(`ALTER TABLE "draft_order" ALTER COLUMN "status" SET DEFAULT 'open'`); + await queryRunner.query(`DROP TYPE "draft_order_status_enum_old"`); + await queryRunner.query(`COMMENT ON COLUMN "draft_order"."status" IS NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`COMMENT ON COLUMN "draft_order"."status" IS NULL`); + await queryRunner.query(`CREATE TYPE "draft_order_status_enum_old" AS ENUM('open', 'awaiting', 'completed')`); + await queryRunner.query(`ALTER TABLE "draft_order" ALTER COLUMN "status" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "draft_order" ALTER COLUMN "status" TYPE "draft_order_status_enum_old" USING "status"::"text"::"draft_order_status_enum_old"`); + await queryRunner.query(`ALTER TABLE "draft_order" ALTER COLUMN "status" SET DEFAULT 'open'`); + await queryRunner.query(`DROP TYPE "draft_order_status_enum"`); + await queryRunner.query(`ALTER TYPE "draft_order_status_enum_old" RENAME TO "draft_order_status_enum"`); + await queryRunner.query(`ALTER TABLE "draft_order" DROP COLUMN "completed_at"`); + } + +} diff --git a/packages/medusa/src/models/draft-order.ts b/packages/medusa/src/models/draft-order.ts index 2406133ff4..85e761ebb4 100644 --- a/packages/medusa/src/models/draft-order.ts +++ b/packages/medusa/src/models/draft-order.ts @@ -17,7 +17,6 @@ import { Order } from "./order" enum DraftOrderStatus { OPEN = "open", - AWAITING = "awaiting", COMPLETED = "completed", } @@ -59,6 +58,9 @@ export class DraftOrder { @UpdateDateColumn({ type: "timestamptz" }) updated_at: Date + @UpdateDateColumn({ type: "timestamptz" }) + completed_at: Date + @Column({ type: "jsonb", nullable: true }) metadata: any @@ -85,7 +87,6 @@ export class DraftOrder { * type: string * enum: * - open - * - awaiting * - completed * display_id: * type: string @@ -111,6 +112,9 @@ export class DraftOrder { * deleted_at: * type: string * format: date-time + * completed_at: + * type: string + * format: date-time * metadata: * type: object * idempotency_key: diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index c49a7b6e55..958b73000d 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -306,7 +306,12 @@ class CartService extends BaseService { const regCountries = region.countries.map(({ iso_2 }) => iso_2) - if (!data.shipping_address && !data.shipping_address_id) { + if (data.shipping_address && typeof data.shipping_address === `string`) { + const addr = await addressRepo.findOne(data.shipping_address) + data.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 @@ -315,22 +320,11 @@ class CartService extends BaseService { }) } } else { - if (data.shipping_address) { - if (!regCountries.includes(data.shipping_address.country_code)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Shipping country not in region" - ) - } - } - if (data.shipping_address_id) { - const addr = await addressRepo.findOne(data.shipping_address_id) - if (!regCountries.includes(addr.country_code)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Shipping country not in region" - ) - } + if (!regCountries.includes(data.shipping_address.country_code)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Shipping country not in region" + ) } } @@ -782,20 +776,32 @@ class CartService extends BaseService { * @param {object} address - the value to set the billing address to * @return {Promise} the result of the update operation */ - async updateBillingAddress_(cart, address, addrRepo) { - address.country_code = address.country_code.toLowerCase() - if (cart.billing_address_id) { - const addr = await addrRepo.findOne({ - where: { id: cart.billing_address_id }, + async updateBillingAddress_(cart, addressOrId, addrRepo) { + if (typeof addressOrId === `string`) { + addressOrId = await addrRepo.findOne({ + where: { id: addressOrId }, }) + } - await addrRepo.save({ ...addr, ...address }) + addressOrId.country_code = addressOrId.country_code.toLowerCase() + + if (addressOrId.id) { + cart.billing_address_id = addressOrId.id + cart.billing_address = addressOrId } else { - const created = addrRepo.create({ - ...address, - }) + if (cart.billing_address_id) { + const addr = await addrRepo.findOne({ + where: { id: cart.billing_address_id }, + }) - cart.billing_address = created + await addrRepo.save({ ...addr, ...addressOrId }) + } else { + const created = addrRepo.create({ + ...addressOrId, + }) + + cart.billing_address = created + } } } @@ -805,11 +811,19 @@ class CartService extends BaseService { * @param {object} address - the value to set the shipping address to * @return {Promise} the result of the update operation */ - async updateShippingAddress_(cart, address, addrRepo) { - address.country_code = address.country_code.toLowerCase() + async updateShippingAddress_(cart, addressOrId, addrRepo) { + if (typeof addressOrId === `string`) { + addressOrId = await addrRepo.findOne({ + where: { id: addressOrId }, + }) + } + + addressOrId.country_code = addressOrId.country_code.toLowerCase() if ( - !cart.region.countries.find(({ iso_2 }) => address.country_code === iso_2) + !cart.region.countries.find( + ({ iso_2 }) => addressOrId.country_code === iso_2 + ) ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -817,18 +831,23 @@ class CartService extends BaseService { ) } - if (cart.shipping_address_id) { - const addr = await addrRepo.findOne({ - where: { id: cart.shipping_address_id }, - }) - - await addrRepo.save({ ...addr, ...address }) + if (addressOrId.id) { + cart.shipping_address = addressOrId + cart.shipping_address_id = addressOrId.id } else { - const created = addrRepo.create({ - ...address, - }) + if (cart.shipping_address_id) { + const addr = await addrRepo.findOne({ + where: { id: cart.shipping_address_id }, + }) - cart.shipping_address = created + await addrRepo.save({ ...addr, ...addressOrId }) + } else { + const created = addrRepo.create({ + ...addressOrId, + }) + + cart.shipping_address = created + } } } diff --git a/packages/medusa/src/services/draft-order.js b/packages/medusa/src/services/draft-order.js index 8abc900f4d..5da84e98cb 100644 --- a/packages/medusa/src/services/draft-order.js +++ b/packages/medusa/src/services/draft-order.js @@ -226,7 +226,6 @@ class DraftOrderService extends BaseService { /** * Creates a draft order. * @param {object} data - data to create draft order from - * @param {boolean} shippingRequired - needs shipping flag * @return {Promise} the created draft order */ async create(data) { @@ -348,6 +347,7 @@ class DraftOrderService extends BaseService { const draftOrder = await this.retrieve(doId) draftOrder.status = "completed" + draftOrder.completed_at = new Date() draftOrder.order_id = orderId await draftOrderRepo.save(draftOrder) diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js index ed9c14a32b..843faae709 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -97,8 +97,12 @@ class LineItemService extends BaseService { const region = await this.regionService_.retrieve(regionId) let price + let shouldMerge = true if (config.unit_price && typeof config.unit_price !== `undefined`) { + // if custom unit_price, we ensure positive values + // and we choose to not merge the items + shouldMerge = false if (config.unit_price < 0) { price = 0 } else { @@ -121,7 +125,7 @@ class LineItemService extends BaseService { allow_discounts: !variant.product.is_giftcard, is_giftcard: variant.product.is_giftcard, metadata: config?.metadata || {}, - should_merge: true, + should_merge: shouldMerge, } return toCreate