From b3b69d711740e54ecc11e79d870ca2872029046c Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Sun, 7 Aug 2022 11:50:12 +0200 Subject: [PATCH 01/34] fix(integration-tests): Use asymmetric matcher for arrays in tests (#1992) --- .changeset/honest-garlics-help.md | 5 + .../api/__tests__/admin/discount.js | 68 +-- .../api/__tests__/admin/order.js | 322 ++++++++------ .../api/__tests__/admin/price-list.js | 130 +++--- .../api/__tests__/admin/product.js | 112 +++-- .../api/__tests__/admin/region.js | 44 +- .../api/__tests__/admin/return-reason.js | 44 +- .../api/__tests__/admin/sales-channels.js | 12 +- .../api/__tests__/returns/index.js | 85 ++-- integration-tests/api/__tests__/store/cart.js | 410 ++++++++++-------- .../api/__tests__/store/products.js | 263 +++++------ .../api/__tests__/store/return-reason.js | 35 +- .../api/__tests__/store/returns.js | 15 +- .../api/__tests__/taxes/orders.js | 52 ++- .../api/__tests__/taxes/shipping-options.js | 34 +- .../api/__tests__/totals/orders.js | 34 +- 16 files changed, 935 insertions(+), 730 deletions(-) create mode 100644 .changeset/honest-garlics-help.md diff --git a/.changeset/honest-garlics-help.md b/.changeset/honest-garlics-help.md new file mode 100644 index 0000000000..79df0a1707 --- /dev/null +++ b/.changeset/honest-garlics-help.md @@ -0,0 +1,5 @@ +--- + +--- + +Use asymetric matcher for arrays diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index 5a56698f7d..6135a7b687 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -331,12 +331,15 @@ describe("/admin/discounts", () => { }) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.discounts).toEqual([ - expect.objectContaining({ - id: "fixed-discount", - code: "fixed100", - }), - ]) + expect(response.data.discounts).toHaveLength(1) + expect(response.data.discounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "fixed-discount", + code: "fixed100", + }), + ]) + ) }) it("fails when listing invalid discount types", async () => { @@ -394,12 +397,15 @@ describe("/admin/discounts", () => { }) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.discounts).toEqual([ - expect.objectContaining({ - id: "dynamic-discount", - code: "Dyn100", - }), - ]) + expect(response.data.discounts).toHaveLength(1) + expect(response.data.discounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "dynamic-discount", + code: "Dyn100", + }) + ]) + ) }) it("lists disabled discounts ", async () => { @@ -416,12 +422,15 @@ describe("/admin/discounts", () => { }) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.discounts).toEqual([ - expect.objectContaining({ - id: "disabled-discount", - code: "Dis100", - }), - ]) + expect(response.data.discounts).toHaveLength(1) + expect(response.data.discounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "disabled-discount", + code: "Dis100", + }), + ]) + ) }) }) @@ -614,16 +623,19 @@ describe("/admin/discounts", () => { }) expect(response.status).toEqual(200) - expect(response.data.discount.rule.conditions).toEqual([ - expect.objectContaining({ - type: "products", - operator: "in", - }), - expect.objectContaining({ - type: "product_types", - operator: "not_in", - }), - ]) + expect(response.data.discount.rule.conditions).toHaveLength(2) + expect(response.data.discount.rule.conditions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "products", + operator: "in", + }), + expect.objectContaining({ + type: "product_types", + operator: "not_in", + }), + ]) + ) const createdRule = response.data.discount.rule const condsToUpdate = createdRule.conditions[0] diff --git a/integration-tests/api/__tests__/admin/order.js b/integration-tests/api/__tests__/admin/order.js index a90b607546..3b11e54614 100644 --- a/integration-tests/api/__tests__/admin/order.js +++ b/integration-tests/api/__tests__/admin/order.js @@ -673,11 +673,14 @@ describe("/admin/orders", () => { ) expect(status).toEqual(200) - expect(updateData.order.claims[0].shipping_methods).toEqual([ - expect.objectContaining({ - id: "test-method", - }), - ]) + expect(updateData.order.claims[0].shipping_methods).toHaveLength(1) + expect(updateData.order.claims[0].shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-method", + }), + ]) + ) }) it("updates claim items", async () => { @@ -827,19 +830,21 @@ describe("/admin/orders", () => { claim = updateData.order.claims[0] expect(claim.claim_items.length).toEqual(1) - expect(claim.claim_items).toEqual([ - expect.objectContaining({ - id: claim.claim_items[0].id, - reason: "production_failure", - note: "Something new", - images: [], - // tags: expect.arrayContaining([ - // expect.objectContaining({ value: "completely" }), - // expect.objectContaining({ value: "new" }), - // expect.objectContaining({ value: "tags" }), - // ]), - }), - ]) + expect(claim.claim_items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: claim.claim_items[0].id, + reason: "production_failure", + note: "Something new", + images: [], + // tags: expect.arrayContaining([ + // expect.objectContaining({ value: "completely" }), + // expect.objectContaining({ value: "new" }), + // expect.objectContaining({ value: "tags" }), + // ]), + }), + ]) + ) }) it("fulfills a claim", async () => { @@ -892,27 +897,34 @@ describe("/admin/orders", () => { } ) expect(fulRes.status).toEqual(200) - expect(fulRes.data.order.claims).toEqual([ - expect.objectContaining({ - id: cid, - order_id: "test-order", - fulfillment_status: "fulfilled", - }), - ]) + expect(fulRes.data.order.claims).toHaveLength(1) + expect(fulRes.data.order.claims).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: cid, + order_id: "test-order", + fulfillment_status: "fulfilled", + }), + ]) + ) const fid = fulRes.data.order.claims[0].fulfillments[0].id const iid = fulRes.data.order.claims[0].additional_items[0].id - expect(fulRes.data.order.claims[0].fulfillments).toEqual([ - expect.objectContaining({ - items: [ - { - fulfillment_id: fid, - item_id: iid, - quantity: 1, - }, - ], - }), - ]) + + expect(fulRes.data.order.claims[0].fulfillments).toHaveLength(1) + expect(fulRes.data.order.claims[0].fulfillments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + items: [ + { + fulfillment_id: fid, + item_id: iid, + quantity: 1, + }, + ], + }), + ]) + ) }) it("creates a claim on a claim additional item", async () => { @@ -1318,14 +1330,17 @@ describe("/admin/orders", () => { expect(response.status).toEqual(200) expect(response.data.order.returns[0].refund_amount).toEqual(7200) - expect(response.data.order.returns[0].items).toEqual([ - expect.objectContaining({ - item_id: "test-item", - quantity: 1, - reason_id: rrId, - note: "TOO SMALL", - }), - ]) + expect(response.data.order.returns[0].items).toHaveLength(1) + expect(response.data.order.returns[0].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: "test-item", + quantity: 1, + reason_id: rrId, + note: "TOO SMALL", + }), + ]) + ) }) it("increases inventory_quantity when return is received", async () => { @@ -1420,28 +1435,31 @@ describe("/admin/orders", () => { }) expect(response.status).toEqual(200) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: "test-order", - }), + expect(response.data.orders).toHaveLength(6) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-order", + }), - expect.objectContaining({ - id: "test-order-w-c", - }), + expect.objectContaining({ + id: "test-order-w-c", + }), - expect.objectContaining({ - id: "test-order-w-s", - }), - expect.objectContaining({ - id: "test-order-w-f", - }), - expect.objectContaining({ - id: "test-order-w-r", - }), - expect.objectContaining({ - id: "discount-order", - }), - ]) + expect.objectContaining({ + id: "test-order-w-s", + }), + expect.objectContaining({ + id: "test-order-w-f", + }), + expect.objectContaining({ + id: "test-order-w-r", + }), + expect.objectContaining({ + id: "discount-order", + }), + ]) + ) }) it("lists all orders with a fulfillment status = fulfilled and payment status = captured", async () => { @@ -1459,14 +1477,17 @@ describe("/admin/orders", () => { .catch((err) => console.log(err)) expect(response.status).toEqual(200) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: "test-order", - }), - expect.objectContaining({ - id: "discount-order", - }), - ]) + expect(response.data.orders).toHaveLength(2) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-order", + }), + expect.objectContaining({ + id: "discount-order", + }), + ]) + ) }) it("fails to lists all orders with an invalid status", async () => { @@ -1502,12 +1523,15 @@ describe("/admin/orders", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: "test-order", - email: "test@email.com", - }), - ]) + expect(response.data.orders).toHaveLength(1) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-order", + email: "test@email.com", + }), + ]) + ) }) it("list all orders with matching shipping_address first name", async () => { @@ -1546,27 +1570,30 @@ describe("/admin/orders", () => { ) expect(response.status).toEqual(200) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: "test-order", - }), - expect.objectContaining({ - id: "test-order-w-c", - }), + expect(response.data.orders).toHaveLength(6) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-order", + }), + expect.objectContaining({ + id: "test-order-w-c", + }), - expect.objectContaining({ - id: "test-order-w-s", - }), - expect.objectContaining({ - id: "test-order-w-f", - }), - expect.objectContaining({ - id: "test-order-w-r", - }), - expect.objectContaining({ - id: "discount-order", - }), - ]) + expect.objectContaining({ + id: "test-order-w-s", + }), + expect.objectContaining({ + id: "test-order-w-f", + }), + expect.objectContaining({ + id: "test-order-w-r", + }), + expect.objectContaining({ + id: "discount-order", + }), + ]) + ) }) it("successfully lists no orders with greater than", async () => { @@ -1598,27 +1625,30 @@ describe("/admin/orders", () => { ) expect(response.status).toEqual(200) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: "test-order", - }), - expect.objectContaining({ - id: "test-order-w-c", - }), + expect(response.data.orders).toHaveLength(6) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-order", + }), + expect.objectContaining({ + id: "test-order-w-c", + }), - expect.objectContaining({ - id: "test-order-w-s", - }), - expect.objectContaining({ - id: "test-order-w-f", - }), - expect.objectContaining({ - id: "test-order-w-r", - }), - expect.objectContaining({ - id: "discount-order", - }), - ]) + expect.objectContaining({ + id: "test-order-w-s", + }), + expect.objectContaining({ + id: "test-order-w-f", + }), + expect.objectContaining({ + id: "test-order-w-r", + }), + expect.objectContaining({ + id: "discount-order", + }), + ]) + ) }) it("successfully lists no orders with less than", async () => { @@ -1650,27 +1680,30 @@ describe("/admin/orders", () => { ) expect(response.status).toEqual(200) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: "test-order", - }), - expect.objectContaining({ - id: "test-order-w-c", - }), + expect(response.data.orders).toHaveLength(6) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-order", + }), + expect.objectContaining({ + id: "test-order-w-c", + }), - expect.objectContaining({ - id: "test-order-w-s", - }), - expect.objectContaining({ - id: "test-order-w-f", - }), - expect.objectContaining({ - id: "test-order-w-r", - }), - expect.objectContaining({ - id: "discount-order", - }), - ]) + expect.objectContaining({ + id: "test-order-w-s", + }), + expect.objectContaining({ + id: "test-order-w-f", + }), + expect.objectContaining({ + id: "test-order-w-r", + }), + expect.objectContaining({ + id: "discount-order", + }), + ]) + ) }) it.each([ @@ -1864,11 +1897,14 @@ describe("/admin/orders", () => { const cart = response.data.cart const items = cart.items const [returnItem] = items.filter((i) => i.is_return) - expect(returnItem.adjustments).toEqual([ - expect.objectContaining({ - amount: -800, - }), - ]) + expect(returnItem.adjustments).toHaveLength(1) + expect(returnItem.adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + amount: -800, + }), + ]) + ) expect(cart.total).toBe(7200) }) }) diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index c5520d7336..47c2acb3aa 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -363,10 +363,13 @@ describe("/admin/price-lists", () => { expect(response.status).toEqual(200) expect(response.data.price_lists.length).toEqual(2) - expect(response.data.price_lists).toEqual([ - expect.objectContaining({ id: "test-list-cgroup-1" }), - expect.objectContaining({ id: "test-list-cgroup-2" }), - ]) + expect(response.data.price_lists).toHaveLength(2) + expect(response.data.price_lists).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: "test-list-cgroup-1" }), + expect.objectContaining({ id: "test-list-cgroup-2" }), + ]) + ) }) }) @@ -1177,52 +1180,55 @@ describe("/admin/price-lists", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(2) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-prod-1", - variants: [ - expect.objectContaining({ - id: "test-variant-1", - prices: [ - expect.objectContaining({ currency_code: "usd", amount: 100 }), - expect.objectContaining({ - currency_code: "usd", - amount: 150, - price_list_id: "test-list", - }), - ], - }), - expect.objectContaining({ - id: "test-variant-2", - prices: [ - expect.objectContaining({ currency_code: "usd", amount: 100 }), - ], - }), - ], - }), - expect.objectContaining({ - id: "test-prod-2", - variants: [ - expect.objectContaining({ - id: "test-variant-3", - prices: [ - expect.objectContaining({ currency_code: "usd", amount: 100 }), - ], - }), - expect.objectContaining({ - id: "test-variant-4", - prices: [ - expect.objectContaining({ currency_code: "usd", amount: 100 }), - expect.objectContaining({ - currency_code: "usd", - amount: 150, - price_list_id: "test-list", - }), - ], - }), - ], - }), - ]) + expect(response.data.products).toHaveLength(2) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-prod-1", + variants: [ + expect.objectContaining({ + id: "test-variant-1", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ], + }), + expect.objectContaining({ + id: "test-variant-2", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ], + }), + ], + }), + expect.objectContaining({ + id: "test-prod-2", + variants: [ + expect.objectContaining({ + id: "test-variant-3", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ], + }), + expect.objectContaining({ + id: "test-variant-4", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ], + }), + ], + }), + ]) + ) }) it("lists only product 2", async () => { @@ -1240,9 +1246,12 @@ describe("/admin/price-lists", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ id: "test-prod-2" }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: "test-prod-2" }), + ]) + ) }) it("lists products using free text search", async () => { @@ -1260,12 +1269,15 @@ describe("/admin/price-lists", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-prod-1", - title: "MedusaHeadphones", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-prod-1", + title: "MedusaHeadphones", + }), + ]) + ) }) }) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 51e11878dd..ecee43a939 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -165,16 +165,19 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_1", - status: "proposed", - }), - expect.objectContaining({ - id: "test-product_filtering_2", - status: "published", - }), - ]) + expect(response.data.products).toHaveLength(2) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_1", + status: "proposed", + }), + expect.objectContaining({ + id: "test-product_filtering_2", + status: "published", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -204,16 +207,19 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_1", - status: "proposed", - }), - expect.objectContaining({ - id: "test-product_filtering_2", - status: "published", - }), - ]) + expect(response.data.products).toHaveLength(2) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_1", + status: "proposed", + }), + expect.objectContaining({ + id: "test-product_filtering_2", + status: "published", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -237,11 +243,14 @@ describe("/admin/products", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_4", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_4", + }), + ]) + ) }) it("returns a list of products with free text query and limit", async () => { @@ -322,11 +331,14 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_4", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_4", + }), + ]) + ) }) it("returns a list of products in collection", async () => { @@ -348,16 +360,19 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_1", - collection_id: "test-collection1", - }), - expect.objectContaining({ - id: "test-product_filtering_3", - collection_id: "test-collection1", - }), - ]) + expect(response.data.products).toHaveLength(2) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_1", + collection_id: "test-collection1", + }), + expect.objectContaining({ + id: "test-product_filtering_3", + collection_id: "test-collection1", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -431,13 +446,16 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_3", - collection_id: "test-collection1", - tags: [expect.objectContaining({ id: "tag4" })], - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_3", + collection_id: "test-collection1", + tags: [expect.objectContaining({ id: "tag4" })], + }), + ]) + ) for (const notExpect of notExpectedCollections) { expect(response.data.products).toEqual( diff --git a/integration-tests/api/__tests__/admin/region.js b/integration-tests/api/__tests__/admin/region.js index fca514edb2..ada14ac77f 100644 --- a/integration-tests/api/__tests__/admin/region.js +++ b/integration-tests/api/__tests__/admin/region.js @@ -162,17 +162,20 @@ describe("/admin/regions", () => { console.log(err) }) - expect(response.data.regions).toEqual([ - expect.objectContaining({ - id: "test-region-updated-1", - }), - expect.objectContaining({ - id: "test-region", - }), - expect.objectContaining({ - id: "test-region-updated", - }), - ]) + expect(response.data.regions).toHaveLength(3) + expect(response.data.regions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-region-updated-1", + }), + expect.objectContaining({ + id: "test-region", + }), + expect.objectContaining({ + id: "test-region-updated", + }), + ]) + ) expect(response.status).toEqual(200) }) @@ -189,14 +192,17 @@ describe("/admin/regions", () => { console.log(err) }) - expect(response.data.regions).toEqual([ - expect.objectContaining({ - id: "test-region", - }), - expect.objectContaining({ - id: "test-region-updated", - }), - ]) + expect(response.data.regions).toHaveLength(2) + expect(response.data.regions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-region", + }), + expect.objectContaining({ + id: "test-region-updated", + }), + ]) + ) expect(response.status).toEqual(200) }) }) diff --git a/integration-tests/api/__tests__/admin/return-reason.js b/integration-tests/api/__tests__/admin/return-reason.js index 4e180e23b6..53679272d5 100644 --- a/integration-tests/api/__tests__/admin/return-reason.js +++ b/integration-tests/api/__tests__/admin/return-reason.js @@ -355,20 +355,23 @@ describe("/admin/return-reasons", () => { expect(nested_response.status).toEqual(200) - expect(nested_response.data.return_reasons).toEqual([ - expect.objectContaining({ - label: "Wrong size", - description: "Use this if the size was too big", - value: "wrong_size", - return_reason_children: expect.arrayContaining([ - expect.objectContaining({ - label: "Too Big", - description: "Use this if the size was too big", - value: "too_big", - }), - ]), - }), - ]) + expect(nested_response.data.return_reasons).toHaveLength(1) + expect(nested_response.data.return_reasons).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + label: "Wrong size", + description: "Use this if the size was too big", + value: "wrong_size", + return_reason_children: expect.arrayContaining([ + expect.objectContaining({ + label: "Too Big", + description: "Use this if the size was too big", + value: "too_big", + }), + ]), + }), + ]) + ) }) it("list return reasons", async () => { @@ -401,11 +404,14 @@ describe("/admin/return-reasons", () => { }) expect(response.status).toEqual(200) - expect(response.data.return_reasons).toEqual([ - expect.objectContaining({ - value: "too_big", - }), - ]) + expect(response.data.return_reasons).toHaveLength(1) + expect(response.data.return_reasons).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: "too_big", + }), + ]) + ) }) }) diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index 7ffebc6ace..46384da339 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -832,11 +832,13 @@ describe("sales channels", () => { expect(response.status).toEqual(200) expect(response.data.orders.length).toEqual(1) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - id: order.id, - }), - ]) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: order.id, + }), + ]) + ) }) }) }) diff --git a/integration-tests/api/__tests__/returns/index.js b/integration-tests/api/__tests__/returns/index.js index 5e7cfa3b83..e2bf989195 100644 --- a/integration-tests/api/__tests__/returns/index.js +++ b/integration-tests/api/__tests__/returns/index.js @@ -72,13 +72,16 @@ describe("/admin/orders", () => { * 1000 * 1.125 = 1125 */ expect(response.data.order.returns[0].refund_amount).toEqual(1125) - expect(response.data.order.returns[0].items).toEqual([ - expect.objectContaining({ - item_id: "test-item", - quantity: 1, - note: "TOO SMALL", - }), - ]) + expect(response.data.order.returns[0].items).toHaveLength(1) + expect(response.data.order.returns[0].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: "test-item", + quantity: 1, + note: "TOO SMALL", + }), + ]) + ) }) test("creates a return w. new tax system", async () => { @@ -113,13 +116,16 @@ describe("/admin/orders", () => { */ expect(response.data.order.returns[0].refund_amount).toEqual(1200) - expect(response.data.order.returns[0].items).toEqual([ - expect.objectContaining({ - item_id: "test-item", - quantity: 1, - note: "TOO SMALL", - }), - ]) + expect(response.data.order.returns[0].items).toHaveLength(1) + expect(response.data.order.returns[0].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: "test-item", + quantity: 1, + note: "TOO SMALL", + }), + ]) + ) }) test("creates a return w. new tax system + shipping", async () => { @@ -163,20 +169,26 @@ describe("/admin/orders", () => { * shipping method will have 12.5 rate 1000 * 1.125 = 1125 */ expect(response.data.order.returns[0].refund_amount).toEqual(75) - expect(response.data.order.returns[0].shipping_method.tax_lines).toEqual([ - expect.objectContaining({ - rate: 12.5, - name: "default", - code: "default", - }), - ]) - expect(response.data.order.returns[0].items).toEqual([ - expect.objectContaining({ - item_id: "test-item", - quantity: 1, - note: "TOO SMALL", - }), - ]) + expect(response.data.order.returns[0].shipping_method.tax_lines).toHaveLength(1) + expect(response.data.order.returns[0].shipping_method.tax_lines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ]) + ) + expect(response.data.order.returns[0].items).toHaveLength(1) + expect(response.data.order.returns[0].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: "test-item", + quantity: 1, + note: "TOO SMALL", + }), + ]) + ) }) test("creates a return w. discount", async () => { @@ -214,13 +226,16 @@ describe("/admin/orders", () => { */ expect(response.data.order.returns[0].refund_amount).toEqual(1080) - expect(response.data.order.returns[0].items).toEqual([ - expect.objectContaining({ - item_id: "test-item", - quantity: 1, - note: "TOO SMALL", - }), - ]) + expect(response.data.order.returns[0].items).toHaveLength(1) + expect(response.data.order.returns[0].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: "test-item", + quantity: 1, + note: "TOO SMALL", + }), + ]) + ) }) test("receives a return with a claimed line item", async () => { diff --git a/integration-tests/api/__tests__/store/cart.js b/integration-tests/api/__tests__/store/cart.js index a8353e2c4e..87ea99a0bf 100644 --- a/integration-tests/api/__tests__/store/cart.js +++ b/integration-tests/api/__tests__/store/cart.js @@ -155,17 +155,20 @@ describe("/store/carts", () => { response.data.cart.items.sort((a, b) => a.quantity - b.quantity) expect(response.status).toEqual(200) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - variant_id: "test-variant_1", - quantity: 1, - }), - expect.objectContaining({ - variant_id: "test-variant-sale", - quantity: 2, - unit_price: 800, - }), - ]) + expect(response.data.cart.items).toHaveLength(2) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + variant_id: "test-variant_1", + quantity: 1, + }), + expect.objectContaining({ + variant_id: "test-variant-sale", + quantity: 2, + unit_price: 800, + }), + ]) + ) const getRes = await api.post(`/store/carts/${response.data.cart.id}`) expect(getRes.status).toEqual(200) @@ -235,15 +238,18 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 1, - adjustments: [], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 1, + adjustments: [], + }), + ]) + ) }) it("adds line item to cart containing a total fixed discount", async () => { @@ -260,21 +266,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-total-fixed-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 2, - adjustments: [ - expect.objectContaining({ - amount: 100, - discount_id: "total-fixed-100", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-total-fixed-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 2, + adjustments: [ + expect.objectContaining({ + amount: 100, + discount_id: "total-fixed-100", + description: "discount", + }), + ], + }), + ]) + ) }) it("adds line item to cart containing a total percentage discount", async () => { @@ -291,21 +300,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-total-percentage-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 2, - adjustments: [ - expect.objectContaining({ - amount: 200, - discount_id: "10Percent", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-total-percentage-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 2, + adjustments: [ + expect.objectContaining({ + amount: 200, + discount_id: "10Percent", + description: "discount", + }), + ], + }), + ]) + ) }) it("adds line item to cart containing an item fixed discount", async () => { @@ -322,21 +334,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-item-fixed-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 2, - adjustments: [ - expect.objectContaining({ - amount: 400, - discount_id: "item-fixed-200", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-item-fixed-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 2, + adjustments: [ + expect.objectContaining({ + amount: 400, + discount_id: "item-fixed-200", + description: "discount", + }), + ], + }), + ]) + ) }) it("adds line item to cart containing an item percentage discount", async () => { @@ -353,21 +368,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-item-percentage-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 2, - adjustments: [ - expect.objectContaining({ - amount: 300, - discount_id: "item-percentage-15", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-item-percentage-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 2, + adjustments: [ + expect.objectContaining({ + amount: 300, + discount_id: "item-percentage-15", + description: "discount", + }), + ], + }), + ]) + ) }) it("adds line item to cart time limited sale", async () => { @@ -384,14 +402,17 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart", - unit_price: 800, - variant_id: "test-variant-sale", - quantity: 1, - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 800, + variant_id: "test-variant-sale", + quantity: 1, + }), + ]) + ) }) it("adds line item to cart time customer pricing", async () => { @@ -422,14 +443,17 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart", - unit_price: 700, - variant_id: "test-variant-sale-customer", - quantity: 1, - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 700, + variant_id: "test-variant-sale-customer", + quantity: 1, + }), + ]) + ) }) it("adds line item with quantity to cart with quantity discount", async () => { @@ -446,14 +470,17 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart", - unit_price: 800, - variant_id: "test-variant-quantity", - quantity: 90, - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 800, + variant_id: "test-variant-quantity", + quantity: 90, + }), + ]) + ) }) it("adds line item with quantity to cart with quantity discount no ceiling", async () => { @@ -470,14 +497,17 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart", - unit_price: 700, - variant_id: "test-variant-quantity", - quantity: 900, - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 700, + variant_id: "test-variant-quantity", + quantity: 900, + }), + ]) + ) }) describe("ensures correct line item adjustment generation", () => { @@ -682,17 +712,19 @@ describe("/store/carts", () => { .catch((err) => console.log(err)) expect(response.data.cart.items.length).toEqual(1) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - adjustments: [ - expect.objectContaining({ - item_id: "line-item-2", - amount: 185, - discount_id: "medusa-185", - }), - ], - }), - ]) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + adjustments: [ + expect.objectContaining({ + item_id: "line-item-2", + amount: 185, + discount_id: "medusa-185", + }), + ], + }), + ]) + ) }) }) }) @@ -761,21 +793,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-total-fixed-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 3, - adjustments: [ - expect.objectContaining({ - amount: 100, - discount_id: "total-fixed-100", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-total-fixed-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 3, + adjustments: [ + expect.objectContaining({ + amount: 100, + discount_id: "total-fixed-100", + description: "discount", + }), + ], + }), + ]) + ) }) it("updates line item of a cart containing a total percentage discount", async () => { @@ -801,21 +836,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-total-percentage-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 10, - adjustments: [ - expect.objectContaining({ - amount: 1000, - discount_id: "10Percent", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-total-percentage-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 10, + adjustments: [ + expect.objectContaining({ + amount: 1000, + discount_id: "10Percent", + description: "discount", + }), + ], + }), + ]) + ) }) it("updates line item of a cart containing an item fixed discount", async () => { @@ -841,21 +879,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-item-fixed-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 4, - adjustments: [ - expect.objectContaining({ - amount: 800, - discount_id: "item-fixed-200", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-item-fixed-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 4, + adjustments: [ + expect.objectContaining({ + amount: 800, + discount_id: "item-fixed-200", + description: "discount", + }), + ], + }), + ]) + ) }) it("updates line item of a cart containing an item percentage discount", async () => { @@ -881,21 +922,24 @@ describe("/store/carts", () => { ) .catch((err) => console.log(err)) - expect(response.data.cart.items).toEqual([ - expect.objectContaining({ - cart_id: "test-cart-w-item-percentage-discount", - unit_price: 1000, - variant_id: "test-variant-quantity", - quantity: 3, - adjustments: [ - expect.objectContaining({ - amount: 450, - discount_id: "item-percentage-15", - description: "discount", - }), - ], - }), - ]) + expect(response.data.cart.items).toHaveLength(1) + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-w-item-percentage-discount", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 3, + adjustments: [ + expect.objectContaining({ + amount: 450, + discount_id: "item-percentage-15", + description: "discount", + }), + ], + }), + ]) + ) }) }) diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index c5a80658fe..7450fe0a03 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -55,12 +55,15 @@ describe("/store/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_2", - collection_id: "test-collection2", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_2", + collection_id: "test-collection2", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -81,12 +84,15 @@ describe("/store/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_1", - collection_id: "test-collection1", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_1", + collection_id: "test-collection1", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -106,12 +112,14 @@ describe("/store/products", () => { expect(response.status).toEqual(200) expect(response.data.products.length).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "giftcard", - is_giftcard: true, - }), - ]) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "giftcard", + is_giftcard: true, + }), + ]) + ) }) it("returns non gift card products", async () => { @@ -144,12 +152,15 @@ describe("/store/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_1", - collection_id: "test-collection1", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_1", + collection_id: "test-collection1", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -172,12 +183,15 @@ describe("/store/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product_filtering_2", - handle: "test-product_filtering_2", - }), - ]) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product_filtering_2", + handle: "test-product_filtering_2", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -201,27 +215,29 @@ describe("/store/products", () => { expect(response.status).toEqual(200) expect(response.data.products.length).toEqual(5) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product1", - collection_id: "test-collection", - }), - expect.objectContaining({ - id: "test-product", - collection_id: "test-collection", - }), - expect.objectContaining({ - id: "test-product_filtering_2", - collection_id: "test-collection2", - }), - expect.objectContaining({ - id: "test-product_filtering_1", - collection_id: "test-collection1", - }), - expect.objectContaining({ - id: "giftcard", - }), - ]) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product1", + collection_id: "test-collection", + }), + expect.objectContaining({ + id: "test-product", + collection_id: "test-collection", + }), + expect.objectContaining({ + id: "test-product_filtering_2", + collection_id: "test-collection2", + }), + expect.objectContaining({ + id: "test-product_filtering_1", + collection_id: "test-collection1", + }), + expect.objectContaining({ + id: "giftcard", + }), + ]) + ) for (const notExpect of notExpected) { expect(response.data.products).toEqual( @@ -256,77 +272,80 @@ describe("/store/products", () => { console.log(err) }) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-product1", - collection_id: "test-collection", - }), - expect.objectContaining({ - id: "test-product", - collection_id: "test-collection", - variants: [ - expect.objectContaining({ - original_price: 100, - calculated_price: 80, - prices: [ - expect.objectContaining({ - id: "test-price", - currency_code: "usd", - amount: 100, - }), - expect.objectContaining({ - id: "test-price-discount", - currency_code: "usd", - amount: 80, - }), - ], - }), - expect.objectContaining({ - original_price: 100, - calculated_price: 80, - prices: [ - expect.objectContaining({ - id: "test-price2", - currency_code: "usd", - amount: 100, - }), - expect.objectContaining({ - id: "test-price2-discount", - currency_code: "usd", - amount: 80, - }), - ], - }), - expect.objectContaining({ - original_price: 100, - calculated_price: 80, - prices: [ - expect.objectContaining({ - id: "test-price1", - currency_code: "usd", - amount: 100, - }), - expect.objectContaining({ - id: "test-price1-discount", - currency_code: "usd", - amount: 80, - }), - ], - }), - ], - }), - expect.objectContaining({ - id: "test-product_filtering_2", - collection_id: "test-collection2", - }), - expect.objectContaining({ - id: "test-product_filtering_1", - collection_id: "test-collection1", - }), - expect.objectContaining({ - id: "giftcard", - }), - ]) + expect(response.data.products).toHaveLength(5) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product1", + collection_id: "test-collection", + }), + expect.objectContaining({ + id: "test-product", + collection_id: "test-collection", + variants: [ + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price2", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price2-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price1", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price1-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), + ], + }), + expect.objectContaining({ + id: "test-product_filtering_2", + collection_id: "test-collection2", + }), + expect.objectContaining({ + id: "test-product_filtering_1", + collection_id: "test-collection1", + }), + expect.objectContaining({ + id: "giftcard", + }), + ]) + ) }) }) diff --git a/integration-tests/api/__tests__/store/return-reason.js b/integration-tests/api/__tests__/store/return-reason.js index 36daf1e302..1d601b3405 100644 --- a/integration-tests/api/__tests__/store/return-reason.js +++ b/integration-tests/api/__tests__/store/return-reason.js @@ -118,22 +118,25 @@ describe("/store/return-reasons", () => { expect(response.status).toEqual(200) - expect(response.data.return_reasons).toEqual([ - expect.objectContaining({ - id: rrId, - value: "wrong_size", - return_reason_children: [ - expect.objectContaining({ - id: rrId_1, - value: "too_big", - }), - ], - }), - expect.objectContaining({ - id: rrId_2, - value: "too_big_1", - }), - ]) + expect(response.data.return_reasons).toHaveLength(2) + expect(response.data.return_reasons).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rrId, + value: "wrong_size", + return_reason_children: [ + expect.objectContaining({ + id: rrId_1, + value: "too_big", + }), + ], + }), + expect.objectContaining({ + id: rrId_2, + value: "too_big_1", + }), + ]) + ) }) }) }) diff --git a/integration-tests/api/__tests__/store/returns.js b/integration-tests/api/__tests__/store/returns.js index 8cee9e0826..95171283ea 100644 --- a/integration-tests/api/__tests__/store/returns.js +++ b/integration-tests/api/__tests__/store/returns.js @@ -241,12 +241,15 @@ describe("/store/carts", () => { }) expect(response.status).toEqual(200) - expect(response.data.return.items).toEqual([ - expect.objectContaining({ - reason_id: rrId_child, - note: "TOO small", - }), - ]) + expect(response.data.return.items).toHaveLength(1) + expect(response.data.return.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reason_id: rrId_child, + note: "TOO small", + }), + ]) + ) }) it("failes to create a return with an invalid quantity (less than 1)", async () => { diff --git a/integration-tests/api/__tests__/taxes/orders.js b/integration-tests/api/__tests__/taxes/orders.js index 2299fb20ff..3c9f434f43 100644 --- a/integration-tests/api/__tests__/taxes/orders.js +++ b/integration-tests/api/__tests__/taxes/orders.js @@ -290,16 +290,22 @@ describe("Order Taxes", () => { response.data.data.items.flatMap((li) => li.tax_lines).length ).toEqual(2) - expect(response.data.data.items[0].tax_lines).toEqual([ - expect.objectContaining({ - rate: 25, - }), - ]) - expect(response.data.data.items[1].tax_lines).toEqual([ - expect.objectContaining({ - rate: 20, - }), - ]) + expect(response.data.data.items[0].tax_lines).toHaveLength(1) + expect(response.data.data.items[0].tax_lines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rate: 25, + }), + ]) + ) + expect(response.data.data.items[1].tax_lines).toHaveLength(1) + expect(response.data.data.items[1].tax_lines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rate: 20, + }), + ]) + ) }) test("completing cart creates tax lines", async () => { @@ -381,15 +387,21 @@ describe("Order Taxes", () => { expect(response.data.data.tax_total).toEqual(35) expect(response.data.data.total).toEqual(185) - expect(response.data.data.items[0].tax_lines).toEqual([ - expect.objectContaining({ - rate: 25, - }), - ]) - expect(response.data.data.items[1].tax_lines).toEqual([ - expect.objectContaining({ - rate: 20, - }), - ]) + expect(response.data.data.items[0].tax_lines).toHaveLength(1) + expect(response.data.data.items[0].tax_lines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rate: 25, + }), + ]) + ) + expect(response.data.data.items[1].tax_lines).toHaveLength(1) + expect(response.data.data.items[1].tax_lines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rate: 20, + }), + ]) + ) }) }) diff --git a/integration-tests/api/__tests__/taxes/shipping-options.js b/integration-tests/api/__tests__/taxes/shipping-options.js index 7f91216db8..85d36d5113 100644 --- a/integration-tests/api/__tests__/taxes/shipping-options.js +++ b/integration-tests/api/__tests__/taxes/shipping-options.js @@ -69,13 +69,16 @@ describe("Shipping Options Totals Calculations", () => { }, }) - expect(res.data.shipping_options).toEqual([ - expect.objectContaining({ - id: so.id, - amount: 100, - price_incl_tax: 110, - }), - ]) + expect(res.data.shipping_options).toHaveLength(1) + expect(res.data.shipping_options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: so.id, + amount: 100, + price_incl_tax: 110, + }), + ]) + ) }) it("gets correct shipping prices", async () => { @@ -98,12 +101,15 @@ describe("Shipping Options Totals Calculations", () => { const res = await api.get(`/store/shipping-options?region_id=${region.id}`) - expect(res.data.shipping_options).toEqual([ - expect.objectContaining({ - id: so.id, - amount: 100, - price_incl_tax: 110, - }), - ]) + expect(res.data.shipping_options).toHaveLength(1) + expect(res.data.shipping_options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: so.id, + amount: 100, + price_incl_tax: 110, + }), + ]) + ) }) }) diff --git a/integration-tests/api/__tests__/totals/orders.js b/integration-tests/api/__tests__/totals/orders.js index 2cb3d2f2fe..cd642a30b7 100644 --- a/integration-tests/api/__tests__/totals/orders.js +++ b/integration-tests/api/__tests__/totals/orders.js @@ -93,13 +93,16 @@ describe("Order Totals", () => { headers: { Authorization: `Bearer test_token` }, }) - expect(data.order.gift_card_transactions).toEqual([ - expect.objectContaining({ - amount: 160000, - is_taxable: false, - tax_rate: null, - }), - ]) + expect(data.order.gift_card_transactions).toHaveLength(1) + expect(data.order.gift_card_transactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + amount: 160000, + is_taxable: false, + tax_rate: null, + }), + ]) + ) expect(data.order.gift_card_total).toEqual(160000) expect(data.order.gift_card_tax_total).toEqual(0) expect(data.order.total).toEqual(59000) @@ -155,13 +158,16 @@ describe("Order Totals", () => { headers: { Authorization: `Bearer test_token` }, }) - expect(data.order.gift_card_transactions).toEqual([ - expect.objectContaining({ - amount: 160000, - is_taxable: true, - tax_rate: 25, - }), - ]) + expect(data.order.gift_card_transactions).toHaveLength(1) + expect(data.order.gift_card_transactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + amount: 160000, + is_taxable: true, + tax_rate: 25, + }), + ]) + ) expect(data.order.gift_card_total).toEqual(160000) expect(data.order.gift_card_tax_total).toEqual(40000) expect(data.order.tax_total).toEqual(3800) From 42ed209518bf0278d1bef3c4c47d0ee21cae84c8 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 7 Aug 2022 17:06:36 +0700 Subject: [PATCH 02/34] feat(medusa): Convert CollectionService to TypeScript (#1976) --- .changeset/fresh-snakes-judge.md | 5 + .../routes/admin/collections/add-products.ts | 13 +- .../admin/collections/create-collection.ts | 10 +- .../admin/collections/update-collection.ts | 11 +- ...ct-collection.js => product-collection.ts} | 173 +++++++++++------- .../medusa/src/types/product-collection.ts | 11 ++ 6 files changed, 138 insertions(+), 85 deletions(-) create mode 100644 .changeset/fresh-snakes-judge.md rename packages/medusa/src/services/{product-collection.js => product-collection.ts} (51%) create mode 100644 packages/medusa/src/types/product-collection.ts diff --git a/.changeset/fresh-snakes-judge.md b/.changeset/fresh-snakes-judge.md new file mode 100644 index 0000000000..ab51fe994e --- /dev/null +++ b/.changeset/fresh-snakes-judge.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Convert CollectionService to TypeScript diff --git a/packages/medusa/src/api/routes/admin/collections/add-products.ts b/packages/medusa/src/api/routes/admin/collections/add-products.ts index a7a859ef25..d1925f2f10 100644 --- a/packages/medusa/src/api/routes/admin/collections/add-products.ts +++ b/packages/medusa/src/api/routes/admin/collections/add-products.ts @@ -1,6 +1,6 @@ import { ArrayNotEmpty, IsString } from "class-validator" import { Request, Response } from "express" -import { EntityManager } from "typeorm"; +import { EntityManager } from "typeorm" import ProductCollectionService from "../../../../services/product-collection" @@ -39,7 +39,9 @@ import ProductCollectionService from "../../../../services/product-collection" */ export default async (req: Request, res: Response) => { const { id } = req.params - const { validatedBody } = req as { validatedBody: AdminPostProductsToCollectionReq } + const { validatedBody } = req as { + validatedBody: AdminPostProductsToCollectionReq + } const productCollectionService: ProductCollectionService = req.scope.resolve( "productCollectionService" @@ -47,10 +49,9 @@ export default async (req: Request, res: Response) => { const manager: EntityManager = req.scope.resolve("manager") const collection = await manager.transaction(async (transactionManager) => { - return await productCollectionService.withTransaction(transactionManager).addProducts( - id, - validatedBody.product_ids - ) + return await productCollectionService + .withTransaction(transactionManager) + .addProducts(id, validatedBody.product_ids) }) res.status(200).json({ collection }) diff --git a/packages/medusa/src/api/routes/admin/collections/create-collection.ts b/packages/medusa/src/api/routes/admin/collections/create-collection.ts index d70e7b3cef..415a7299d6 100644 --- a/packages/medusa/src/api/routes/admin/collections/create-collection.ts +++ b/packages/medusa/src/api/routes/admin/collections/create-collection.ts @@ -1,7 +1,7 @@ import { IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator" import ProductCollectionService from "../../../../services/product-collection" import { Request, Response } from "express" -import { EntityManager } from "typeorm"; +import { EntityManager } from "typeorm" /** * @oas [post] /collections @@ -38,7 +38,7 @@ import { EntityManager } from "typeorm"; * $ref: "#/components/schemas/product_collection" */ export default async (req: Request, res: Response) => { - const { validatedBody } = req + const { validatedBody } = req as { validatedBody: AdminPostCollectionsReq } const productCollectionService: ProductCollectionService = req.scope.resolve( "productCollectionService" @@ -46,7 +46,9 @@ export default async (req: Request, res: Response) => { const manager: EntityManager = req.scope.resolve("manager") const created = await manager.transaction(async (transactionManager) => { - return await productCollectionService.withTransaction(transactionManager).create(validatedBody) + return await productCollectionService + .withTransaction(transactionManager) + .create(validatedBody) }) const collection = await productCollectionService.retrieve(created.id) @@ -65,5 +67,5 @@ export class AdminPostCollectionsReq { @IsObject() @IsOptional() - metadata?: object + metadata?: Record } diff --git a/packages/medusa/src/api/routes/admin/collections/update-collection.ts b/packages/medusa/src/api/routes/admin/collections/update-collection.ts index 84a3c020d1..d7773136e6 100644 --- a/packages/medusa/src/api/routes/admin/collections/update-collection.ts +++ b/packages/medusa/src/api/routes/admin/collections/update-collection.ts @@ -1,6 +1,5 @@ import { IsObject, IsOptional, IsString } from "class-validator" import { Request, Response } from "express" - import { EntityManager } from "typeorm"; import ProductCollectionService from "../../../../services/product-collection" @@ -40,7 +39,9 @@ import ProductCollectionService from "../../../../services/product-collection" */ export default async (req: Request, res: Response) => { const { id } = req.params - const { validatedBody } = req + const { validatedBody } = req as { + validatedBody: AdminPostCollectionsCollectionReq + } const productCollectionService: ProductCollectionService = req.scope.resolve( "productCollectionService" @@ -48,7 +49,9 @@ export default async (req: Request, res: Response) => { const manager: EntityManager = req.scope.resolve("manager") const updated = await manager.transaction(async (transactionManager) => { - return await productCollectionService.withTransaction(transactionManager).update(id, validatedBody) + return await productCollectionService + .withTransaction(transactionManager) + .update(id, validatedBody) }) const collection = await productCollectionService.retrieve(updated.id) @@ -67,5 +70,5 @@ export class AdminPostCollectionsCollectionReq { @IsObject() @IsOptional() - metadata?: object + metadata?: Record } diff --git a/packages/medusa/src/services/product-collection.js b/packages/medusa/src/services/product-collection.ts similarity index 51% rename from packages/medusa/src/services/product-collection.js rename to packages/medusa/src/services/product-collection.ts index 53422a5e2f..c95e17ef10 100644 --- a/packages/medusa/src/services/product-collection.js +++ b/packages/medusa/src/services/product-collection.ts @@ -1,65 +1,71 @@ import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" -import { Brackets, ILike } from "typeorm" +import { Brackets, EntityManager, ILike } from "typeorm" +import { TransactionBaseService } from "../interfaces" +import { ProductCollection } from "../models" +import { ProductRepository } from "../repositories/product" +import { ProductCollectionRepository } from "../repositories/product-collection" +import { ExtendedFindConfig, FindConfig, QuerySelector } from "../types/common" +import { + CreateProductCollection, + UpdateProductCollection +} from "../types/product-collection" +import { buildQuery, setMetadata } from "../utils" import { formatException } from "../utils/exception-formatter" +import EventBusService from "./event-bus" + +type InjectedDependencies = { + manager: EntityManager + eventBusService: EventBusService + productRepository: typeof ProductRepository + productCollectionRepository: typeof ProductCollectionRepository +} /** * Provides layer to manipulate product collections. - * @extends BaseService */ -class ProductCollectionService extends BaseService { +class ProductCollectionService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly eventBus_: EventBusService + + protected readonly productCollectionRepository_: typeof ProductCollectionRepository + protected readonly productRepository_: typeof ProductRepository + constructor({ manager, productCollectionRepository, productRepository, eventBusService, - }) { - super() - - /** @private @const {EntityManager} */ + }: InjectedDependencies) { + super({ + manager, + productCollectionRepository, + productRepository, + eventBusService, + }) this.manager_ = manager - /** @private @const {ProductCollectionRepository} */ this.productCollectionRepository_ = productCollectionRepository - - /** @private @const {ProductRepository} */ this.productRepository_ = productRepository - - /** @private @const {EventBus} */ this.eventBus_ = eventBusService } - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new ProductCollectionService({ - manager: transactionManager, - productCollectionRepository: this.productCollectionRepository_, - productRepository: this.productRepository_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - /** * Retrieves a product collection by id. - * @param {string} collectionId - the id of the collection to retrieve. - * @param {Object} config - the config of the collection to retrieve. - * @return {Promise} the collection. + * @param collectionId - the id of the collection to retrieve. + * @param config - the config of the collection to retrieve. + * @return the collection. */ - async retrieve(collectionId, config = {}) { + async retrieve( + collectionId: string, + config: FindConfig = {} + ): Promise { const collectionRepo = this.manager_.getCustomRepository( this.productCollectionRepository_ ) - const validatedId = this.validateId_(collectionId) - - const query = this.buildQuery_({ id: validatedId }, config) + const query = buildQuery({ id: collectionId }, config) const collection = await collectionRepo.findOne(query) if (!collection) { @@ -74,16 +80,19 @@ class ProductCollectionService extends BaseService { /** * Retrieves a product collection by id. - * @param {string} collectionHandle - the handle of the collection to retrieve. - * @param {object} config - query config for request - * @return {Promise} the collection. + * @param collectionHandle - the handle of the collection to retrieve. + * @param config - query config for request + * @return the collection. */ - async retrieveByHandle(collectionHandle, config = {}) { + async retrieveByHandle( + collectionHandle: string, + config: FindConfig = {} + ): Promise { const collectionRepo = this.manager_.getCustomRepository( this.productCollectionRepository_ ) - const query = this.buildQuery_({ handle: collectionHandle }, config) + const query = buildQuery({ handle: collectionHandle }, config) const collection = await collectionRepo.findOne(query) if (!collection) { @@ -98,11 +107,13 @@ class ProductCollectionService extends BaseService { /** * Creates a product collection - * @param {object} collection - the collection to create - * @return {Promise} created collection + * @param collection - the collection to create + * @return created collection */ - async create(collection) { - return this.atomicPhase_(async (manager) => { + async create( + collection: CreateProductCollection + ): Promise { + return await this.atomicPhase_(async (manager) => { const collectionRepo = manager.getCustomRepository( this.productCollectionRepository_ ) @@ -118,12 +129,15 @@ class ProductCollectionService extends BaseService { /** * Updates a product collection - * @param {string} collectionId - id of collection to update - * @param {object} update - update object - * @return {Promise} update collection + * @param collectionId - id of collection to update + * @param update - update object + * @return update collection */ - async update(collectionId, update) { - return this.atomicPhase_(async (manager) => { + async update( + collectionId: string, + update: UpdateProductCollection + ): Promise { + return await this.atomicPhase_(async (manager) => { const collectionRepo = manager.getCustomRepository( this.productCollectionRepository_ ) @@ -133,7 +147,7 @@ class ProductCollectionService extends BaseService { const { metadata, ...rest } = update if (metadata) { - collection.metadata = this.setMetadata_(collection, metadata) + collection.metadata = setMetadata(collection, metadata) } for (const [key, value] of Object.entries(rest)) { @@ -146,11 +160,11 @@ class ProductCollectionService extends BaseService { /** * Deletes a product collection idempotently - * @param {string} collectionId - id of collection to delete - * @return {Promise} empty promise + * @param collectionId - id of collection to delete + * @return empty promise */ - async delete(collectionId) { - return this.atomicPhase_(async (manager) => { + async delete(collectionId: string): Promise { + return await this.atomicPhase_(async (manager) => { const productCollectionRepo = manager.getCustomRepository( this.productCollectionRepository_ ) @@ -167,8 +181,11 @@ class ProductCollectionService extends BaseService { }) } - async addProducts(collectionId, productIds) { - return this.atomicPhase_(async (manager) => { + async addProducts( + collectionId: string, + productIds: string[] + ): Promise { + return await this.atomicPhase_(async (manager) => { const productRepo = manager.getCustomRepository(this.productRepository_) try { @@ -185,8 +202,11 @@ class ProductCollectionService extends BaseService { }) } - async removeProducts(collectionId, productIds) { - return this.atomicPhase_(async (manager) => { + async removeProducts( + collectionId: string, + productIds: string[] + ): Promise { + return await this.atomicPhase_(async (manager) => { const productRepo = manager.getCustomRepository(this.productRepository_) const { id } = await this.retrieve(collectionId, { select: ["id"] }) @@ -199,26 +219,32 @@ class ProductCollectionService extends BaseService { /** * Lists product collections - * @param {Object} selector - the query object for find - * @param {Object} config - the config to be used for find - * @return {Promise} the result of the find operation + * @param selector - the query object for find + * @param config - the config to be used for find + * @return the result of the find operation */ - async list(selector = {}, config = { skip: 0, take: 20 }) { + async list( + selector = {}, + config = { skip: 0, take: 20 } + ): Promise { const productCollectionRepo = this.manager_.getCustomRepository( this.productCollectionRepository_ ) - const query = this.buildQuery_(selector, config) + const query = buildQuery(selector, config) return await productCollectionRepo.find(query) } /** * Lists product collections and add count. - * @param {Object} selector - the query object for find - * @param {Object} config - the config to be used for find - * @return {Promise} the result of the find operation + * @param selector - the query object for find + * @param config - the config to be used for find + * @return the result of the find operation */ - async listAndCount(selector = {}, config = { skip: 0, take: 20 }) { + async listAndCount( + selector: QuerySelector = {}, + config: FindConfig = { skip: 0, take: 20 } + ): Promise<[ProductCollection[], number]> { const productCollectionRepo = this.manager_.getCustomRepository( this.productCollectionRepository_ ) @@ -229,7 +255,12 @@ class ProductCollectionService extends BaseService { delete selector.q } - const query = this.buildQuery_(selector, config) + const query = buildQuery( + selector, + config + ) as ExtendedFindConfig & { + where: (qb: any) => void + } if (q) { const where = query.where @@ -239,7 +270,7 @@ class ProductCollectionService extends BaseService { delete where.created_at delete where.updated_at - query.where = (qb) => { + query.where = (qb): void => { qb.where(where) qb.andWhere( diff --git a/packages/medusa/src/types/product-collection.ts b/packages/medusa/src/types/product-collection.ts new file mode 100644 index 0000000000..17dca44e03 --- /dev/null +++ b/packages/medusa/src/types/product-collection.ts @@ -0,0 +1,11 @@ +export type CreateProductCollection = { + title: string + handle?: string + metadata?: Record +} + +export type UpdateProductCollection = { + title?: string + handle?: string + metadata?: Record +} From 11fab121f4c4b5ec3b6a3afccd4c44844bc5e3d9 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 7 Aug 2022 18:15:20 +0700 Subject: [PATCH 03/34] feat(medusa): Convert OauthService to TypeScript (#1983) --- .changeset/sixty-boats-flow.md | 5 + packages/medusa/src/services/oauth.js | 121 ----------------- packages/medusa/src/services/oauth.ts | 178 ++++++++++++++++++++++++++ packages/medusa/src/types/oauth.ts | 10 ++ 4 files changed, 193 insertions(+), 121 deletions(-) create mode 100644 .changeset/sixty-boats-flow.md delete mode 100644 packages/medusa/src/services/oauth.js create mode 100644 packages/medusa/src/services/oauth.ts create mode 100644 packages/medusa/src/types/oauth.ts diff --git a/.changeset/sixty-boats-flow.md b/.changeset/sixty-boats-flow.md new file mode 100644 index 0000000000..b3a144bb7e --- /dev/null +++ b/.changeset/sixty-boats-flow.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Convert OauthService to TypeScript diff --git a/packages/medusa/src/services/oauth.js b/packages/medusa/src/services/oauth.js deleted file mode 100644 index e1de71075b..0000000000 --- a/packages/medusa/src/services/oauth.js +++ /dev/null @@ -1,121 +0,0 @@ -import { MedusaError } from "medusa-core-utils" -import { OauthService } from "medusa-interfaces" - -class Oauth extends OauthService { - static Events = { - TOKEN_GENERATED: "oauth.token_generated", - TOKEN_REFRESHED: "oauth.token_refreshed", - } - - constructor(cradle) { - super() - const manager = cradle.manager - - this.manager = manager - this.container_ = cradle - this.oauthRepository_ = cradle.oauthRepository - this.eventBus_ = cradle.eventBusService - } - - retrieveByName(appName) { - const repo = this.manager.getCustomRepository(this.oauthRepository_) - return repo.findOne({ - application_name: appName, - }) - } - - list(selector) { - const repo = this.manager.getCustomRepository(this.oauthRepository_) - return repo.find(selector) - } - - async create(data) { - const repo = this.manager.getCustomRepository(this.oauthRepository_) - - const application = repo.create({ - display_name: data.display_name, - application_name: data.application_name, - install_url: data.install_url, - uninstall_url: data.uninstall_url, - }) - - return repo.save(application) - } - - async update(id, update) { - const repo = this.manager.getCustomRepository(this.oauthRepository_) - const oauth = await repo.findOne({ where: { id } }) - - if ("data" in update) { - oauth.data = update.data - } - - return repo.save(oauth) - } - - async registerOauthApp(appDetails) { - const { application_name } = appDetails - const existing = await this.retrieveByName(application_name) - if (existing) { - return - } - - return this.create(appDetails) - } - - async generateToken(appName, code, state) { - const app = await this.retrieveByName(appName) - const service = this.container_[`${app.application_name}Oauth`] - if (!service) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `An OAuth handler for ${app.display_name} could not be found make sure the plugin is installed` - ) - } - - if (!app.state === state) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `${app.display_name} could not match state` - ) - } - - const authData = await service.generateToken(code) - - return this.update(app.id, { - data: authData, - }).then((result) => { - this.eventBus_.emit( - `${Oauth.Events.TOKEN_GENERATED}.${appName}`, - authData - ) - return result - }) - } - - async refreshToken(appName) { - const app = await this.retrieveByName(appName) - const refreshToken = app.data.refresh_token - const service = this.container_[`${app.application_name}Oauth`] - if (!service) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `An OAuth handler for ${app.display_name} could not be found make sure the plugin is installed` - ) - } - - const authData = await service.refreshToken(refreshToken) - - return this.update(app.id, { - data: authData, - }).then((result) => { - this.eventBus_.emit( - `${Oauth.Events.TOKEN_REFRESHED}.${appName}`, - authData - ) - return result - }) - } -} - -export default Oauth diff --git a/packages/medusa/src/services/oauth.ts b/packages/medusa/src/services/oauth.ts new file mode 100644 index 0000000000..8a819c7305 --- /dev/null +++ b/packages/medusa/src/services/oauth.ts @@ -0,0 +1,178 @@ +import { MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" +import { Oauth as OAuthModel } from "../models" +import { OauthRepository } from "../repositories/oauth" +import { Selector } from "../types/common" +import { MedusaContainer } from "../types/global" +import { CreateOauthInput, UpdateOauthInput } from "../types/oauth" +import { buildQuery } from "../utils" +import EventBusService from "./event-bus" + +type InjectedDependencies = MedusaContainer & { + manager: EntityManager + eventBusService: EventBusService + oauthRepository: typeof OauthRepository +} + +class Oauth extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + static Events = { + TOKEN_GENERATED: "oauth.token_generated", + TOKEN_REFRESHED: "oauth.token_refreshed", + } + + protected manager: EntityManager + protected container_: InjectedDependencies + protected oauthRepository_: typeof OauthRepository + protected eventBus_: EventBusService + + constructor(cradle: InjectedDependencies) { + super(cradle) + const manager = cradle.manager + + this.manager = manager + this.container_ = cradle + this.oauthRepository_ = cradle.oauthRepository + this.eventBus_ = cradle.eventBusService + } + + async retrieveByName(appName: string): Promise { + const repo = this.manager.getCustomRepository(this.oauthRepository_) + const oauth = await repo.findOne({ + application_name: appName, + }) + + if (!oauth) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Oauth application ${appName} not found` + ) + } + + return oauth + } + + async retrieve(oauthId: string): Promise { + const repo = this.manager.getCustomRepository(this.oauthRepository_) + const oauth = await repo.findOne({ + id: oauthId, + }) + + if (!oauth) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Oauth application with id ${oauthId} not found` + ) + } + + return oauth + } + + async list(selector: Selector): Promise { + const repo = this.manager.getCustomRepository(this.oauthRepository_) + + const query = buildQuery(selector, {}) + + return await repo.find(query) + } + + async create(data: CreateOauthInput): Promise { + return await this.atomicPhase_(async (manager) => { + const repo = manager.getCustomRepository(this.oauthRepository_) + + const application = repo.create({ + display_name: data.display_name, + application_name: data.application_name, + install_url: data.install_url, + uninstall_url: data.uninstall_url, + }) + + return await repo.save(application) + }) + } + + async update(id: string, update: UpdateOauthInput): Promise { + return await this.atomicPhase_(async (manager) => { + const repo = manager.getCustomRepository(this.oauthRepository_) + const oauth = await this.retrieve(id) + + if ("data" in update) { + oauth.data = update.data + } + + return await repo.save(oauth) + }) + } + + async registerOauthApp(appDetails: CreateOauthInput): Promise { + const { application_name } = appDetails + const existing = await this.retrieveByName(application_name) + if (existing) { + return existing + } + + return await this.create(appDetails) + } + + async generateToken( + appName: string, + code: string, + state: string + ): Promise { + const app = await this.retrieveByName(appName) + const service = this.container_[`${app.application_name}Oauth`] + if (!service) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An OAuth handler for ${app.display_name} could not be found make sure the plugin is installed` + ) + } + + if (!(app.data.state === state)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `${app.display_name} could not match state` + ) + } + + const authData = await service.generateToken(code) + + return await this.update(app.id, { + data: authData, + }).then(async (result) => { + await this.eventBus_.emit( + `${Oauth.Events.TOKEN_GENERATED}.${appName}`, + authData + ) + return result + }) + } + + async refreshToken(appName: string): Promise { + const app = await this.retrieveByName(appName) + const refreshToken = app.data.refresh_token + const service = this.container_[`${app.application_name}Oauth`] + if (!service) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An OAuth handler for ${app.display_name} could not be found make sure the plugin is installed` + ) + } + + const authData = await service.refreshToken(refreshToken) + + return await this.update(app.id, { + data: authData, + }).then(async (result) => { + await this.eventBus_.emit( + `${Oauth.Events.TOKEN_REFRESHED}.${appName}`, + authData + ) + return result + }) + } +} + +export default Oauth diff --git a/packages/medusa/src/types/oauth.ts b/packages/medusa/src/types/oauth.ts new file mode 100644 index 0000000000..508660a837 --- /dev/null +++ b/packages/medusa/src/types/oauth.ts @@ -0,0 +1,10 @@ +export type CreateOauthInput = { + display_name: string + application_name: string + install_url?: string + uninstall_url?: string +} + +export type UpdateOauthInput = { + data: Record +} From a88bf3c76ea801d2b17227fb2eb8b8d8dbfe1262 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Sun, 7 Aug 2022 13:22:36 +0200 Subject: [PATCH 04/34] feat(medusa-js): Add Collection batch (remove, add) endpoints (#1958) --- .changeset/wild-tables-compete.md | 5 +++ .../src/resources/admin/collections.ts | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 .changeset/wild-tables-compete.md diff --git a/.changeset/wild-tables-compete.md b/.changeset/wild-tables-compete.md new file mode 100644 index 0000000000..31de16fad0 --- /dev/null +++ b/.changeset/wild-tables-compete.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa-js": patch +--- + +Add batch endpoints (remove, add) for Collections to medusa-js diff --git a/packages/medusa-js/src/resources/admin/collections.ts b/packages/medusa-js/src/resources/admin/collections.ts index 3b161dd335..b42d2cceed 100644 --- a/packages/medusa-js/src/resources/admin/collections.ts +++ b/packages/medusa-js/src/resources/admin/collections.ts @@ -5,6 +5,8 @@ import { AdminCollectionsDeleteRes, AdminCollectionsListRes, AdminGetCollectionsParams, + AdminPostProductsToCollectionReq, + AdminDeleteProductsFromCollectionReq, } from "@medusajs/medusa" import qs from "qs" import { ResponsePromise } from "../../typings" @@ -88,6 +90,36 @@ class AdminCollectionsResource extends BaseResource { return this.client.request("GET", path, undefined, {}, customHeaders) } + + /** + * @description Updates products associated with a Product Collection + * @param id the id of the Collection + * @param payload - an object which contains an array of Product IDs to add to the Product Collection + * @param customHeaders + */ + addProducts( + id: string, + payload: AdminPostProductsToCollectionReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/collections/${id}/products/batch` + return this.client.request("POST", path, payload, {}, customHeaders) + } + + /** + * @description Removes products associated with a Product Collection + * @param id - the id of the Collection + * @param payload - an object which contains an array of Product IDs to add to the Product Collection + * @param customHeaders + */ + removeProducts( + id: string, + payload: AdminDeleteProductsFromCollectionReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/collections/${id}/products/batch` + return this.client.request("DELETE", path, payload, {}, customHeaders) + } } export default AdminCollectionsResource From 2a723dcd4fb0074e7d34286c231ef248e907b1c4 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Mon, 8 Aug 2022 12:58:23 +0200 Subject: [PATCH 05/34] feat(medusa-react): Add Collection batch (remove, add) endpoints (#1959) --- .changeset/gorgeous-bears-hear.md | 5 ++ packages/medusa-react/mocks/handlers/admin.ts | 23 ++++++++ .../src/hooks/admin/collections/mutations.ts | 59 +++++++++++++++++++ .../hooks/admin/collections/mutations.test.ts | 56 ++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 .changeset/gorgeous-bears-hear.md diff --git a/.changeset/gorgeous-bears-hear.md b/.changeset/gorgeous-bears-hear.md new file mode 100644 index 0000000000..9429ff0bca --- /dev/null +++ b/.changeset/gorgeous-bears-hear.md @@ -0,0 +1,5 @@ +--- +"medusa-react": patch +--- + +Add Collection batch (remove, add) endpoints to medusa-react diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index bd98e0dcf5..0f861bd7e5 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -108,6 +108,29 @@ export const adminHandlers = [ ) }), + rest.post("/admin/collections/:id/products/batch", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + collection: { + ...fixtures.get("product_collection"), + products: [fixtures.get("product")] + } + }) + ) + }), + + rest.delete("/admin/collections/:id/products/batch", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + id: req.params.id, + object: "product-collection", + removed_products: [fixtures.get("product").id] + }) + ) + }), + rest.post("/admin/gift-cards/", (req, res, ctx) => { const body = req.body as Record return res( diff --git a/packages/medusa-react/src/hooks/admin/collections/mutations.ts b/packages/medusa-react/src/hooks/admin/collections/mutations.ts index 9871d695b4..df134276e0 100644 --- a/packages/medusa-react/src/hooks/admin/collections/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/collections/mutations.ts @@ -1,8 +1,10 @@ import { AdminCollectionsDeleteRes, AdminCollectionsRes, + AdminDeleteProductsFromCollectionReq, AdminPostCollectionsCollectionReq, AdminPostCollectionsReq, + AdminPostProductsToCollectionReq, } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { useMutation, UseMutationOptions, useQueryClient } from "react-query" @@ -62,3 +64,60 @@ export const useAdminDeleteCollection = ( ) ) } + + +/** + * Hook returns function for adding multiple products to a collection. + * + * @param id - id of the collection in which products are being added + * @param options + */ +export const useAdminAddProductsToCollection = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostProductsToCollectionReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminPostProductsToCollectionReq) => + client.admin.collections.addProducts(id, payload), + buildOptions( + queryClient, + [adminCollectionKeys.lists(), adminCollectionKeys.detail(id)], + options + ) + ) +} + +/** + * Hook returns function for removal of multiple products from a collection. + * + * @param id - id of the collection from which products will be removed + * @param options + */ +export const useAdminRemoveProductsFromCollection = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminDeleteProductsFromCollectionReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminDeleteProductsFromCollectionReq) => + client.admin.collections.removeProducts(id, payload), + buildOptions( + queryClient, + [adminCollectionKeys.lists(), adminCollectionKeys.detail(id)], + options + ) + ) +} diff --git a/packages/medusa-react/test/hooks/admin/collections/mutations.test.ts b/packages/medusa-react/test/hooks/admin/collections/mutations.test.ts index e18743dfb6..85a679d3f5 100644 --- a/packages/medusa-react/test/hooks/admin/collections/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/collections/mutations.test.ts @@ -2,6 +2,8 @@ import { useAdminCreateCollection, useAdminUpdateCollection, useAdminDeleteCollection, + useAdminAddProductsToCollection, + useAdminRemoveProductsFromCollection, } from "../../../../src/" import { renderHook } from "@testing-library/react-hooks" import { fixtures } from "../../../../mocks/data" @@ -80,3 +82,57 @@ describe("useAdminDeleteCollection hook", () => { ) }) }) + +describe("useAdminAddProductsToCollection hook", () => { + test("add products to a collection", async () => { + const update = { + product_ids: [fixtures.get("product").id], + } + + const { result, waitFor } = renderHook( + () => useAdminAddProductsToCollection(fixtures.get("product_collection").id), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(update) + + await waitFor(() => result.current.isSuccess) + expect(result.current.data?.response.status).toEqual(200) + expect(result.current.data?.collection).toEqual( + expect.objectContaining({ + ...fixtures.get("product_collection"), + products: [fixtures.get("product")], + }) + ) + }) +}) + +describe("useAdminRemoveProductsFromCollection hook", () => { + test("remove products from a collection", async () => { + const remove = { + product_ids: [fixtures.get("product").id], + } + + const { result, waitFor } = renderHook( + () => useAdminRemoveProductsFromCollection(fixtures.get("product_collection").id), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(remove) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data?.response.status).toEqual(200) + expect(result.current.data).toEqual( + expect.objectContaining({ + id: fixtures.get("product_collection").id, + object: "product-collection", + removed_products: remove.product_ids + }) + ) + }) +}) From 40ae53567a23ebe562e571fa22f1721eed174c82 Mon Sep 17 00:00:00 2001 From: chemicalkosek Date: Mon, 8 Aug 2022 13:02:33 +0200 Subject: [PATCH 06/34] feat(medusa-payment-stripe): Add support for Przelewy24 and Blik (#1982) --- .changeset/stale-mice-sip.md | 6 + .../src/services/stripe-blik.js | 241 ++++++++++++++++++ .../src/services/stripe-przelewy24.js | 241 ++++++++++++++++++ 3 files changed, 488 insertions(+) create mode 100644 .changeset/stale-mice-sip.md create mode 100644 packages/medusa-payment-stripe/src/services/stripe-blik.js create mode 100644 packages/medusa-payment-stripe/src/services/stripe-przelewy24.js diff --git a/.changeset/stale-mice-sip.md b/.changeset/stale-mice-sip.md new file mode 100644 index 0000000000..d6a4cb15f2 --- /dev/null +++ b/.changeset/stale-mice-sip.md @@ -0,0 +1,6 @@ +--- +"medusa-payment-stripe": patch +"@medusajs/medusa": patch +--- + +Add payment providers Przelewy24 and Blik through Stripe diff --git a/packages/medusa-payment-stripe/src/services/stripe-blik.js b/packages/medusa-payment-stripe/src/services/stripe-blik.js new file mode 100644 index 0000000000..fc7e07845a --- /dev/null +++ b/packages/medusa-payment-stripe/src/services/stripe-blik.js @@ -0,0 +1,241 @@ +import _ from "lodash" +import Stripe from "stripe" +import { PaymentService } from "medusa-interfaces" + +class BlikProviderService extends PaymentService { + static identifier = "stripe-blik" + + constructor( + { stripeProviderService, customerService, totalsService, regionService }, + options + ) { + super() + + /** + * Required Stripe options: + * { + * api_key: "stripe_secret_key", REQUIRED + * webhook_secret: "stripe_webhook_secret", REQUIRED + * // Use this flag to capture payment immediately (default is false) + * capture: true + * } + */ + this.options_ = options + + /** @private @const {Stripe} */ + this.stripe_ = Stripe(options.api_key) + + /** @private @const {CustomerService} */ + this.stripeProviderService_ = stripeProviderService + + /** @private @const {CustomerService} */ + this.customerService_ = customerService + + /** @private @const {RegionService} */ + this.regionService_ = regionService + + /** @private @const {TotalsService} */ + this.totalsService_ = totalsService + } + + /** + * Fetches Stripe payment intent. Check its status and returns the + * corresponding Medusa status. + * @param {object} paymentData - payment method data from cart + * @returns {string} the status of the payment intent + */ + async getStatus(paymentData) { + return await this.stripeProviderService_.getStatus(paymentData) + } + + /** + * Fetches a customers saved payment methods if registered in Stripe. + * @param {object} customer - customer to fetch saved cards for + * @returns {Promise>} saved payments methods + */ + async retrieveSavedMethods(customer) { + return Promise.resolve([]) + } + + /** + * Fetches a Stripe customer + * @param {string} customerId - Stripe customer id + * @returns {Promise} Stripe customer + */ + async retrieveCustomer(customerId) { + return await this.stripeProviderService_.retrieveCustomer(customerId) + } + + /** + * Creates a Stripe customer using a Medusa customer. + * @param {object} customer - Customer data from Medusa + * @returns {Promise} Stripe customer + */ + async createCustomer(customer) { + return await this.stripeProviderService_.createCustomer(customer) + } + + /** + * Creates a Stripe payment intent. + * If customer is not registered in Stripe, we do so. + * @param {object} cart - cart to create a payment for + * @returns {object} Stripe payment intent + */ + async createPayment(cart) { + const { customer_id, region_id, email } = cart + const region = await this.regionService_.retrieve(region_id) + const { currency_code } = region + + const amount = await this.totalsService_.getTotal(cart) + + const intentRequest = { + amount: Math.round(amount), + currency: currency_code, + payment_method_types: ["blik"], + capture_method: "automatic", + metadata: { cart_id: `${cart.id}` }, + } + + if (customer_id) { + const customer = await this.customerService_.retrieve(customer_id) + + if (customer.metadata?.stripe_id) { + intentRequest.customer = customer.metadata.stripe_id + } else { + const stripeCustomer = await this.createCustomer({ + email, + id: customer_id, + }) + + intentRequest.customer = stripeCustomer.id + } + } else { + const stripeCustomer = await this.createCustomer({ + email, + }) + + intentRequest.customer = stripeCustomer.id + } + + const paymentIntent = await this.stripe_.paymentIntents.create( + intentRequest + ) + + return paymentIntent + } + + /** + * Retrieves Stripe payment intent. + * @param {object} data - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent + */ + async retrievePayment(data) { + return await this.stripeProviderService_.retrievePayment(data) + } + + /** + * Gets a Stripe payment intent and returns it. + * @param {object} sessionData - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent + */ + async getPaymentData(sessionData) { + return await this.stripeProviderService_.getPaymentData(sessionData) + } + + /** + * Authorizes Stripe payment intent by simply returning + * the status for the payment intent in use. + * @param {object} sessionData - payment session data + * @param {object} context - properties relevant to current context + * @returns {Promise<{ status: string, data: object }>} result with data and status + */ + async authorizePayment(sessionData, context = {}) { + return await this.stripeProviderService_.authorizePayment( + sessionData, + context + ) + } + + async updatePaymentData(sessionData, update) { + return await this.stripeProviderService_.updatePaymentData( + sessionData, + update + ) + } + + /** + * Updates Stripe payment intent. + * @param {object} sessionData - payment session data. + * @param {object} update - objec to update intent with + * @returns {object} Stripe payment intent + */ + async updatePayment(sessionData, cart) { + try { + const stripeId = cart.customer?.metadata?.stripe_id || undefined + + if (stripeId !== sessionData.customer) { + return this.createPayment(cart) + } else { + if (cart.total && sessionData.amount === Math.round(cart.total)) { + return sessionData + } + + return this.stripe_.paymentIntents.update(sessionData.id, { + amount: Math.round(cart.total), + }) + } + } catch (error) { + throw error + } + } + + async deletePayment(payment) { + return await this.stripeProviderService_.deletePayment(payment) + } + + /** + * Updates customer of Stripe payment intent. + * @param {string} paymentIntentId - id of payment intent to update + * @param {string} customerId - id of new Stripe customer + * @returns {object} Stripe payment intent + */ + async updatePaymentIntentCustomer(paymentIntentId, customerId) { + return await this.stripeProviderService_.updatePaymentIntentCustomer( + paymentIntentId, + customerId + ) + } + + /** + * Captures payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @returns {object} Stripe payment intent + */ + async capturePayment(payment) { + return await this.stripeProviderService_.capturePayment(payment) + } + + /** + * Refunds payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @param {number} amountToRefund - amount to refund + * @returns {string} refunded payment intent + */ + async refundPayment(payment, amountToRefund) { + return await this.stripeProviderService_.refundPayment( + payment, + amountToRefund + ) + } + + /** + * Cancels payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @returns {object} canceled payment intent + */ + async cancelPayment(payment) { + return await this.stripeProviderService_.cancelPayment(payment) + } +} + +export default BlikProviderService diff --git a/packages/medusa-payment-stripe/src/services/stripe-przelewy24.js b/packages/medusa-payment-stripe/src/services/stripe-przelewy24.js new file mode 100644 index 0000000000..82f7f2a534 --- /dev/null +++ b/packages/medusa-payment-stripe/src/services/stripe-przelewy24.js @@ -0,0 +1,241 @@ +import _ from "lodash" +import Stripe from "stripe" +import { PaymentService } from "medusa-interfaces" + +class Przelewy24ProviderService extends PaymentService { + static identifier = "stripe-przelewy24" + + constructor( + { stripeProviderService, customerService, totalsService, regionService }, + options + ) { + super() + + /** + * Required Stripe options: + * { + * api_key: "stripe_secret_key", REQUIRED + * webhook_secret: "stripe_webhook_secret", REQUIRED + * // Use this flag to capture payment immediately (default is false) + * capture: true + * } + */ + this.options_ = options + + /** @private @const {Stripe} */ + this.stripe_ = Stripe(options.api_key) + + /** @private @const {CustomerService} */ + this.stripeProviderService_ = stripeProviderService + + /** @private @const {CustomerService} */ + this.customerService_ = customerService + + /** @private @const {RegionService} */ + this.regionService_ = regionService + + /** @private @const {TotalsService} */ + this.totalsService_ = totalsService + } + + /** + * Fetches Stripe payment intent. Check its status and returns the + * corresponding Medusa status. + * @param {object} paymentData - payment method data from cart + * @returns {string} the status of the payment intent + */ + async getStatus(paymentData) { + return await this.stripeProviderService_.getStatus(paymentData) + } + + /** + * Fetches a customers saved payment methods if registered in Stripe. + * @param {object} customer - customer to fetch saved cards for + * @returns {Promise>} saved payments methods + */ + async retrieveSavedMethods(customer) { + return Promise.resolve([]) + } + + /** + * Fetches a Stripe customer + * @param {string} customerId - Stripe customer id + * @returns {Promise} Stripe customer + */ + async retrieveCustomer(customerId) { + return await this.stripeProviderService_.retrieveCustomer(customerId) + } + + /** + * Creates a Stripe customer using a Medusa customer. + * @param {object} customer - Customer data from Medusa + * @returns {Promise} Stripe customer + */ + async createCustomer(customer) { + return await this.stripeProviderService_.createCustomer(customer) + } + + /** + * Creates a Stripe payment intent. + * If customer is not registered in Stripe, we do so. + * @param {object} cart - cart to create a payment for + * @returns {object} Stripe payment intent + */ + async createPayment(cart) { + const { customer_id, region_id, email } = cart + const region = await this.regionService_.retrieve(region_id) + const { currency_code } = region + + const amount = await this.totalsService_.getTotal(cart) + + const intentRequest = { + amount: Math.round(amount), + currency: currency_code, + payment_method_types: ["p24"], + capture_method: "automatic", + metadata: { cart_id: `${cart.id}` }, + } + + if (customer_id) { + const customer = await this.customerService_.retrieve(customer_id) + + if (customer.metadata?.stripe_id) { + intentRequest.customer = customer.metadata.stripe_id + } else { + const stripeCustomer = await this.createCustomer({ + email, + id: customer_id, + }) + + intentRequest.customer = stripeCustomer.id + } + } else { + const stripeCustomer = await this.createCustomer({ + email, + }) + + intentRequest.customer = stripeCustomer.id + } + + const paymentIntent = await this.stripe_.paymentIntents.create( + intentRequest + ) + + return paymentIntent + } + + /** + * Retrieves Stripe payment intent. + * @param {object} data - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent + */ + async retrievePayment(data) { + return await this.stripeProviderService_.retrievePayment(data) + } + + /** + * Gets a Stripe payment intent and returns it. + * @param {object} sessionData - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent + */ + async getPaymentData(sessionData) { + return await this.stripeProviderService_.getPaymentData(sessionData) + } + + /** + * Authorizes Stripe payment intent by simply returning + * the status for the payment intent in use. + * @param {object} sessionData - payment session data + * @param {object} context - properties relevant to current context + * @returns {Promise<{ status: string, data: object }>} result with data and status + */ + async authorizePayment(sessionData, context = {}) { + return await this.stripeProviderService_.authorizePayment( + sessionData, + context + ) + } + + async updatePaymentData(sessionData, update) { + return await this.stripeProviderService_.updatePaymentData( + sessionData, + update + ) + } + + /** + * Updates Stripe payment intent. + * @param {object} sessionData - payment session data. + * @param {object} update - objec to update intent with + * @returns {object} Stripe payment intent + */ + async updatePayment(sessionData, cart) { + try { + const stripeId = cart.customer?.metadata?.stripe_id || undefined + + if (stripeId !== sessionData.customer) { + return this.createPayment(cart) + } else { + if (cart.total && sessionData.amount === Math.round(cart.total)) { + return sessionData + } + + return this.stripe_.paymentIntents.update(sessionData.id, { + amount: Math.round(cart.total), + }) + } + } catch (error) { + throw error + } + } + + async deletePayment(payment) { + return await this.stripeProviderService_.deletePayment(payment) + } + + /** + * Updates customer of Stripe payment intent. + * @param {string} paymentIntentId - id of payment intent to update + * @param {string} customerId - id of new Stripe customer + * @returns {object} Stripe payment intent + */ + async updatePaymentIntentCustomer(paymentIntentId, customerId) { + return await this.stripeProviderService_.updatePaymentIntentCustomer( + paymentIntentId, + customerId + ) + } + + /** + * Captures payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @returns {object} Stripe payment intent + */ + async capturePayment(payment) { + return await this.stripeProviderService_.capturePayment(payment) + } + + /** + * Refunds payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @param {number} amountToRefund - amount to refund + * @returns {string} refunded payment intent + */ + async refundPayment(payment, amountToRefund) { + return await this.stripeProviderService_.refundPayment( + payment, + amountToRefund + ) + } + + /** + * Cancels payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @returns {object} canceled payment intent + */ + async cancelPayment(payment) { + return await this.stripeProviderService_.cancelPayment(payment) + } +} + +export default Przelewy24ProviderService From 152934f8b07cb3095788091df6823f9665fdf43d Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 8 Aug 2022 21:51:55 +0700 Subject: [PATCH 07/34] feat(medusa): Convert ShippingProfileService to TypeScript (#1963) --- .changeset/proud-papayas-enjoy.md | 5 + .../routes/admin/shipping-profiles/index.ts | 8 +- .../services/__tests__/shipping-profile.js | 18 +- .../medusa/src/services/shipping-profile.js | 489 ------------------ .../medusa/src/services/shipping-profile.ts | 481 +++++++++++++++++ packages/medusa/src/types/common.ts | 16 +- packages/medusa/src/types/shipping-options.ts | 1 + packages/medusa/src/types/shipping-profile.ts | 13 + 8 files changed, 519 insertions(+), 512 deletions(-) create mode 100644 .changeset/proud-papayas-enjoy.md delete mode 100644 packages/medusa/src/services/shipping-profile.js create mode 100644 packages/medusa/src/services/shipping-profile.ts create mode 100644 packages/medusa/src/types/shipping-profile.ts diff --git a/.changeset/proud-papayas-enjoy.md b/.changeset/proud-papayas-enjoy.md new file mode 100644 index 0000000000..2db53f1019 --- /dev/null +++ b/.changeset/proud-papayas-enjoy.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Convert ShippingProfileService to TypeScript diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts b/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts index 3437f22bcc..ae8d503966 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts @@ -31,7 +31,7 @@ export default (app) => { return app } -export const defaultAdminShippingProfilesFields = [ +export const defaultAdminShippingProfilesFields: (keyof ShippingProfile)[] = [ "id", "name", "type", @@ -43,10 +43,8 @@ export const defaultAdminShippingProfilesFields = [ export type AdminDeleteShippingProfileRes = DeleteResponse -export const defaultAdminShippingProfilesRelations = [ - "products", - "shipping_options", -] +export const defaultAdminShippingProfilesRelations: (keyof ShippingProfile)[] = + ["products", "shipping_options"] export type AdminShippingProfilesRes = { shipping_profile: ShippingProfile diff --git a/packages/medusa/src/services/__tests__/shipping-profile.js b/packages/medusa/src/services/__tests__/shipping-profile.js index f9cc55ebc1..42b5a2e040 100644 --- a/packages/medusa/src/services/__tests__/shipping-profile.js +++ b/packages/medusa/src/services/__tests__/shipping-profile.js @@ -29,21 +29,21 @@ describe("ShippingProfileService", () => { describe("update", () => { const profRepo = MockRepository({ - findOne: q => { + findOne: (q) => { return Promise.resolve({ id: q.where.id }) }, }) const productService = { update: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } const shippingOptionService = { update: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } @@ -98,7 +98,7 @@ describe("ShippingProfileService", () => { describe("delete", () => { const profRepo = MockRepository({ - findOne: q => { + findOne: (q) => { return Promise.resolve({ id: q.where.id }) }, }) @@ -126,7 +126,7 @@ describe("ShippingProfileService", () => { const productService = { update: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } @@ -156,7 +156,7 @@ describe("ShippingProfileService", () => { describe("fetchCartOptions", () => { const profRepo = MockRepository({ - find: q => { + find: (q) => { switch (q.where.id) { default: return Promise.resolve([ @@ -188,8 +188,8 @@ describe("ShippingProfileService", () => { }, ]) }), - validateCartOption: jest.fn().mockImplementation(s => s), - withTransaction: function() { + validateCartOption: jest.fn().mockImplementation((s) => s), + withTransaction: function () { return this }, } @@ -301,7 +301,7 @@ describe("ShippingProfileService", () => { const shippingOptionService = { update: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } diff --git a/packages/medusa/src/services/shipping-profile.js b/packages/medusa/src/services/shipping-profile.js deleted file mode 100644 index dfba1ba59a..0000000000 --- a/packages/medusa/src/services/shipping-profile.js +++ /dev/null @@ -1,489 +0,0 @@ -import _ from "lodash" -import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" -import { Any } from "typeorm" - -/** - * Provides layer to manipulate profiles. - * @class - * @implements {BaseService} - */ -class ShippingProfileService extends BaseService { - constructor({ - manager, - shippingProfileRepository, - productService, - productRepository, - shippingOptionService, - customShippingOptionService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {ShippingProfileRepository} */ - this.shippingProfileRepository_ = shippingProfileRepository - - /** @private @const {ProductService} */ - this.productService_ = productService - - /** @private @const {ProductReppsitory} */ - this.productRepository_ = productRepository - - /** @private @const {ShippingOptionService} */ - this.shippingOptionService_ = shippingOptionService - - /** @private @const {CustomShippingOptionService} */ - this.customShippingOptionService_ = customShippingOptionService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new ShippingProfileService({ - manager: transactionManager, - shippingProfileRepository: this.shippingProfileRepository_, - productService: this.productService_, - shippingOptionService: this.shippingOptionService_, - customShippingOptionService: this.customShippingOptionService_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - - /** - * @param {Object} selector - the query object for find - * @param {Object} config - the config object for find - * @return {Promise} the result of the find operation - */ - async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { - const shippingProfileRepo = this.manager_.getCustomRepository( - this.shippingProfileRepository_ - ) - - const query = this.buildQuery_(selector, config) - return shippingProfileRepo.find(query) - } - - async fetchOptionsByProductIds(productIds, filter) { - const products = await this.productService_.list( - { - id: Any(productIds), - }, - { - relations: [ - "profile", - "profile.shipping_options", - "profile.shipping_options.requirements", - ], - } - ) - - const profiles = products.map((p) => p.profile) - - const optionIds = profiles.reduce( - (acc, next) => acc.concat(next.shipping_options), - [] - ) - - const options = await Promise.all( - optionIds.map(async (option) => { - let canSend = true - if (filter.region_id) { - if (filter.region_id !== option.region_id) { - canSend = false - } - } - - if (option.deleted_at !== null) { - canSend = false - } - - return canSend ? option : null - }) - ) - - return options.filter((o) => !!o) - } - - /** - * Gets a profile by id. - * Throws in case of DB Error and if profile was not found. - * @param {string} profileId - the id of the profile to get. - * @param {Object} options - options opf the query. - * @return {Promise} the profile document. - */ - async retrieve(profileId, options = {}) { - const profileRepository = this.manager_.getCustomRepository( - this.shippingProfileRepository_ - ) - const validatedId = this.validateId_(profileId) - - const query = { - where: { id: validatedId }, - } - - if (options.select) { - query.select = options.select - } - - if (options.relations) { - query.relations = options.relations - } - - const profile = await profileRepository.findOne(query) - - if (!profile) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Profile with id: ${profileId} was not found` - ) - } - - return profile - } - - async retrieveDefault() { - const profileRepository = this.manager_.getCustomRepository( - this.shippingProfileRepository_ - ) - - const profile = await profileRepository.findOne({ - where: { type: "default" }, - }) - - return profile - } - - /** - * Creates a default shipping profile, if this does not already exist. - * @return {Promise} the shipping profile - */ - async createDefault() { - return this.atomicPhase_(async (manager) => { - let profile = await this.retrieveDefault() - - if (!profile) { - const profileRepository = manager.getCustomRepository( - this.shippingProfileRepository_ - ) - - const p = await profileRepository.create({ - type: "default", - name: "Default Shipping Profile", - }) - - profile = await profileRepository.save(p) - } - - return profile - }) - } - - /** - * Retrieves the default gift card profile - * @return {Object} the shipping profile for gift cards - */ - async retrieveGiftCardDefault() { - const profileRepository = this.manager_.getCustomRepository( - this.shippingProfileRepository_ - ) - - const giftCardProfile = await profileRepository.findOne({ - where: { type: "gift_card" }, - }) - - return giftCardProfile - } - - /** - * Creates a default shipping profile, for gift cards if unless it already - * exists. - * @return {Promise} the shipping profile - */ - async createGiftCardDefault() { - return this.atomicPhase_(async (manager) => { - let profile = await this.retrieveGiftCardDefault() - - if (!profile) { - const profileRepository = manager.getCustomRepository( - this.shippingProfileRepository_ - ) - - const p = await profileRepository.create({ - type: "gift_card", - name: "Gift Card Profile", - }) - - profile = await profileRepository.save(p) - } - - return profile - }) - } - - /** - * Creates a new shipping profile. - * @param {ShippingProfile} profile - the shipping profile to create from - * @return {Promise} the result of the create operation - */ - async create(profile) { - return this.atomicPhase_(async (manager) => { - const profileRepository = manager.getCustomRepository( - this.shippingProfileRepository_ - ) - - if (profile.products || profile.shipping_options) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Please add products and shipping_options after creating Shipping Profiles" - ) - } - - const created = profileRepository.create(profile) - const result = await profileRepository.save(created) - return result - }) - } - - /** - * Updates a profile. Metadata updates and product updates should use - * dedicated methods, e.g. `setMetadata`, `addProduct`, etc. The function - * will throw errors if metadata or product updates are attempted. - * @param {string} profileId - the id of the profile. Must be a string that - * can be casted to an ObjectId - * @param {object} update - an object with the update values. - * @return {Promise} resolves to the update result. - */ - async update(profileId, update) { - return this.atomicPhase_(async (manager) => { - const profileRepository = manager.getCustomRepository( - this.shippingProfileRepository_ - ) - - const profile = await this.retrieve(profileId, { - relations: [ - "products", - "products.profile", - "shipping_options", - "shipping_options.profile", - ], - }) - - const { metadata, products, shipping_options, ...rest } = update - - if (metadata) { - profile.metadata = this.setMetadata_(profile, metadata) - } - - if (products) { - for (const pId of products) { - await this.productService_.withTransaction(manager).update(pId, { - profile_id: profile.id, - }) - } - } - - if (shipping_options) { - for (const oId of shipping_options) { - await this.shippingOptionService_ - .withTransaction(manager) - .update(oId, { - profile_id: profile.id, - }) - } - } - - for (const [key, value] of Object.entries(rest)) { - profile[key] = value - } - - const result = await profileRepository.save(profile) - return result - }) - } - - /** - * Deletes a profile with a given profile id. - * @param {string} profileId - the id of the profile to delete. Must be - * castable as an ObjectId - * @return {Promise} the result of the delete operation. - */ - async delete(profileId) { - return this.atomicPhase_(async (manager) => { - const profileRepo = manager.getCustomRepository( - this.shippingProfileRepository_ - ) - - // Should not fail, if profile does not exist, since delete is idempotent - const profile = await profileRepo.findOne({ where: { id: profileId } }) - - if (!profile) { - return Promise.resolve() - } - - await profileRepo.softRemove(profile) - - return Promise.resolve() - }) - } - - /** - * Adds a product to a profile. The method is idempotent, so multiple calls - * with the same product variant will have the same result. - * @param {string} profileId - the profile to add the product to. - * @param {string} productId - the product to add. - * @return {Promise} the result of update - */ - async addProduct(profileId, productId) { - return this.atomicPhase_(async (manager) => { - await this.productService_ - .withTransaction(manager) - .update(productId, { profile_id: profileId }) - - const updated = await this.retrieve(profileId) - return updated - }) - } - - /** - * Adds a shipping option to the profile. The shipping option can be used to - * fulfill the products in the products field. - * @param {string} profileId - the profile to apply the shipping option to - * @param {string} optionId - the option to add to the profile - * @return {Promise} the result of the model update operation - */ - async addShippingOption(profileId, optionId) { - return this.atomicPhase_(async (manager) => { - await this.shippingOptionService_ - .withTransaction(manager) - .update(optionId, { profile_id: profileId }) - - const updated = await this.retrieve(profileId) - return updated - }) - } - - /** - * Decorates a profile. - * @param {Profile} profile - the profile to decorate. - * @param {string[]} fields - the fields to include. - * @param {string[]} expandFields - fields to expand. - * @return {Profile} return the decorated profile. - */ - async decorate(profile, fields, expandFields = []) { - const requiredFields = ["_id", "metadata"] - const decorated = _.pick(profile, fields.concat(requiredFields)) - - if (expandFields.includes("products") && profile.products) { - decorated.products = await Promise.all( - profile.products.map((pId) => this.productService_.retrieve(pId)) - ) - } - - if (expandFields.includes("shipping_options") && profile.shipping_options) { - decorated.shipping_options = await Promise.all( - profile.shipping_options.map((oId) => - this.shippingOptionService_.retrieve(oId) - ) - ) - } - - const final = await this.runDecorators_(decorated) - return final - } - - /** - * Returns a list of all the productIds in the cart. - * @param {Cart} cart - the cart to extract products from - * @return {[string]} a list of product ids - */ - getProfilesInCart_(cart) { - return cart.items.reduce((acc, next) => { - // We may have line items that are not associated with a product - if (next.variant && next.variant.product) { - if (!acc.includes(next.variant.product.profile_id)) { - acc.push(next.variant.product.profile_id) - } - } - - return acc - }, []) - } - - /** - * Finds all the shipping profiles that cover the products in a cart, and - * validates all options that are available for the cart. - * @param {Cart} cart - the cart object to find shipping options for - * @return {Promise<[ShippingOption]>} a list of the available shipping options - */ - async fetchCartOptions(cart) { - const profileIds = this.getProfilesInCart_(cart) - - const selector = { - profile_id: profileIds, - admin_only: false, - } - - const customShippingOptions = await this.customShippingOptionService_ - .withTransaction(this.manager_) - .list( - { - cart_id: cart.id, - }, - { select: ["id", "shipping_option_id", "price"] } - ) - - const hasCustomShippingOptions = customShippingOptions?.length - // if there are custom shipping options associated with the cart, use those - if (hasCustomShippingOptions) { - selector.id = customShippingOptions.map((cso) => cso.shipping_option_id) - } - - const rawOpts = await this.shippingOptionService_ - .withTransaction(this.manager_) - .list(selector, { - relations: ["requirements", "profile"], - }) - - // if there are custom shipping options associated with the cart, return cart shipping options with custom price - if (hasCustomShippingOptions) { - return rawOpts.map((so) => { - const customOption = customShippingOptions.find( - (cso) => cso.shipping_option_id === so.id - ) - - return { - ...so, - amount: customOption?.price, - } - }) - } - - const options = await Promise.all( - rawOpts.map(async (so) => { - try { - const option = await this.shippingOptionService_ - .withTransaction(this.manager_) - .validateCartOption(so, cart) - if (option) { - return option - } - return null - } catch (err) { - // if validateCartOption fails it means the option is not valid - return null - } - }) - ) - - return options.filter(Boolean) - } -} - -export default ShippingProfileService diff --git a/packages/medusa/src/services/shipping-profile.ts b/packages/medusa/src/services/shipping-profile.ts new file mode 100644 index 0000000000..5d4a8cb15c --- /dev/null +++ b/packages/medusa/src/services/shipping-profile.ts @@ -0,0 +1,481 @@ +import { MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" +import { + Cart, + ShippingOption, + ShippingProfile, + ShippingProfileType, +} from "../models" +import { ProductRepository } from "../repositories/product" +import { ShippingProfileRepository } from "../repositories/shipping-profile" +import { FindConfig, Selector } from "../types/common" +import { + CreateShippingProfile, + UpdateShippingProfile, +} from "../types/shipping-profile" +import { buildQuery, setMetadata } from "../utils" +import CustomShippingOptionService from "./custom-shipping-option" +import ProductService from "./product" +import ShippingOptionService from "./shipping-option" + +type InjectedDependencies = { + manager: EntityManager + productService: ProductService + shippingOptionService: ShippingOptionService + customShippingOptionService: CustomShippingOptionService + shippingProfileRepository: typeof ShippingProfileRepository + productRepository: typeof ProductRepository +} +/** + * Provides layer to manipulate profiles. + * @constructor + * @implements {BaseService} + */ +class ShippingProfileService extends TransactionBaseService { + protected readonly productService_: ProductService + protected readonly shippingOptionService_: ShippingOptionService + protected readonly customShippingOptionService_: CustomShippingOptionService + protected readonly shippingProfileRepository_: typeof ShippingProfileRepository + protected readonly productRepository_: typeof ProductRepository + + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + constructor({ + manager, + shippingProfileRepository, + productService, + productRepository, + shippingOptionService, + customShippingOptionService, + }: InjectedDependencies) { + super({ + manager, + shippingProfileRepository, + productService, + productRepository, + shippingOptionService, + customShippingOptionService, + }) + + this.manager_ = manager + this.shippingProfileRepository_ = shippingProfileRepository + this.productService_ = productService + this.productRepository_ = productRepository + this.shippingOptionService_ = shippingOptionService + this.customShippingOptionService_ = customShippingOptionService + } + + /** + * @param selector - the query object for find + * @param config - the config object for find + * @return the result of the find operation + */ + async list( + selector: Selector = {}, + config: FindConfig = { relations: [], skip: 0, take: 10 } + ): Promise { + const shippingProfileRepo = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) + + const query = buildQuery(selector, config) + return shippingProfileRepo.find(query) + } + + async fetchOptionsByProductIds( + productIds: string[], + filter: Selector + ): Promise { + const products = await this.productService_.list( + { + id: productIds, + }, + { + relations: [ + "profile", + "profile.shipping_options", + "profile.shipping_options.requirements", + ], + } + ) + + const profiles = products.map((p) => p.profile) + + const shippingOptions = profiles.reduce( + (acc: ShippingOption[], next: ShippingProfile) => + acc.concat(next.shipping_options), + [] + ) + + const options = await Promise.all( + shippingOptions.map(async (option) => { + let canSend = true + if (filter.region_id) { + if (filter.region_id !== option.region_id) { + canSend = false + } + } + + if (option.deleted_at !== null) { + canSend = false + } + + return canSend ? option : null + }) + ) + + return options.filter(Boolean) as ShippingOption[] + } + + /** + * Gets a profile by id. + * Throws in case of DB Error and if profile was not found. + * @param profileId - the id of the profile to get. + * @param options - options opf the query. + * @return {Promise} the profile document. + */ + async retrieve( + profileId: string, + options: FindConfig = {} + ): Promise { + const profileRepository = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) + + const query = buildQuery({ id: profileId }, options) + + const profile = await profileRepository.findOne(query) + + if (!profile) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Profile with id: ${profileId} was not found` + ) + } + + return profile + } + + async retrieveDefault(): Promise { + const profileRepository = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) + + const profile = await profileRepository.findOne({ + where: { type: "default" }, + }) + + return profile + } + + /** + * Creates a default shipping profile, if this does not already exist. + * @return {Promise} the shipping profile + */ + async createDefault(): Promise { + return await this.atomicPhase_(async (manager) => { + let profile = await this.retrieveDefault() + + if (!profile) { + const profileRepository = manager.getCustomRepository( + this.shippingProfileRepository_ + ) + + const toCreate = { + type: ShippingProfileType.DEFAULT, + name: "Default Shipping Profile", + } + + const created = await profileRepository.create(toCreate) + + profile = await profileRepository.save(created) + } + + return profile + }) + } + + /** + * Retrieves the default gift card profile + * @return the shipping profile for gift cards + */ + async retrieveGiftCardDefault(): Promise { + const profileRepository = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) + + const giftCardProfile = await profileRepository.findOne({ + where: { type: "gift_card" }, + }) + + return giftCardProfile + } + + /** + * Creates a default shipping profile, for gift cards if unless it already + * exists. + * @return the shipping profile + */ + async createGiftCardDefault(): Promise { + return await this.atomicPhase_(async (manager) => { + let profile = await this.retrieveGiftCardDefault() + + if (!profile) { + const profileRepository = manager.getCustomRepository( + this.shippingProfileRepository_ + ) + + const created = await profileRepository.create({ + type: ShippingProfileType.GIFT_CARD, + name: "Gift Card Profile", + }) + + profile = await profileRepository.save(created) + } + + return profile + }) + } + + /** + * Creates a new shipping profile. + * @param profile - the shipping profile to create from + * @return the result of the create operation + */ + async create(profile: CreateShippingProfile): Promise { + return await this.atomicPhase_(async (manager) => { + const profileRepository = manager.getCustomRepository( + this.shippingProfileRepository_ + ) + + if (profile["products"] || profile["shipping_options"]) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Please add products and shipping_options after creating Shipping Profiles" + ) + } + + const created = profileRepository.create(profile) + const result = await profileRepository.save(created) + return result + }) + } + + /** + * Updates a profile. Metadata updates and product updates should use + * dedicated methods, e.g. `setMetadata`, `addProduct`, etc. The function + * will throw errors if metadata or product updates are attempted. + * @param profileId - the id of the profile. Must be a string that + * can be casted to an ObjectId + * @param update - an object with the update values. + * @return resolves to the update result. + */ + async update( + profileId: string, + update: UpdateShippingProfile + ): Promise { + return await this.atomicPhase_(async (manager) => { + const profileRepository = manager.getCustomRepository( + this.shippingProfileRepository_ + ) + + const profile = await this.retrieve(profileId, { + relations: [ + "products", + "products.profile", + "shipping_options", + "shipping_options.profile", + ], + }) + + const { metadata, products, shipping_options, ...rest } = update + + if (metadata) { + profile.metadata = setMetadata(profile, metadata) + } + + if (products) { + for (const pId of products) { + await this.productService_.withTransaction(manager).update(pId, { + profile_id: profile.id, + }) + } + } + + if (shipping_options) { + for (const oId of shipping_options) { + await this.shippingOptionService_ + .withTransaction(manager) + .update(oId, { + profile_id: profile.id, + }) + } + } + + for (const [key, value] of Object.entries(rest)) { + profile[key] = value + } + + return await profileRepository.save(profile) + }) + } + + /** + * Deletes a profile with a given profile id. + * @param profileId - the id of the profile to delete. Must be + * castable as an ObjectId + * @return the result of the delete operation. + */ + async delete(profileId: string): Promise { + return await this.atomicPhase_(async (manager) => { + const profileRepo = manager.getCustomRepository( + this.shippingProfileRepository_ + ) + + // Should not fail, if profile does not exist, since delete is idempotent + const profile = await profileRepo.findOne({ where: { id: profileId } }) + + if (!profile) { + return Promise.resolve() + } + + await profileRepo.softRemove(profile) + + return Promise.resolve() + }) + } + + /** + * Adds a product to a profile. The method is idempotent, so multiple calls + * with the same product variant will have the same result. + * @param profileId - the profile to add the product to. + * @param productId - the product to add. + * @return the result of update + */ + async addProduct( + profileId: string, + productId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { + await this.productService_ + .withTransaction(manager) + .update(productId, { profile_id: profileId }) + + return await this.retrieve(profileId) + }) + } + + /** + * Adds a shipping option to the profile. The shipping option can be used to + * fulfill the products in the products field. + * @param profileId - the profile to apply the shipping option to + * @param optionId - the option to add to the profile + * @return the result of the model update operation + */ + async addShippingOption( + profileId: string, + optionId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { + await this.shippingOptionService_ + .withTransaction(manager) + .update(optionId, { profile_id: profileId }) + + const updated = await this.retrieve(profileId) + return updated + }) + } + + /** + * Finds all the shipping profiles that cover the products in a cart, and + * validates all options that are available for the cart. + * @param cart - the cart object to find shipping options for + * @return a list of the available shipping options + */ + async fetchCartOptions(cart): Promise { + return await this.atomicPhase_(async (manager) => { + const profileIds = this.getProfilesInCart(cart) + + const selector: Selector = { + profile_id: profileIds, + admin_only: false, + } + + const customShippingOptions = await this.customShippingOptionService_ + .withTransaction(manager) + .list( + { + cart_id: cart.id, + }, + { select: ["id", "shipping_option_id", "price"] } + ) + + const hasCustomShippingOptions = customShippingOptions?.length + // if there are custom shipping options associated with the cart, use those + if (hasCustomShippingOptions) { + selector.id = customShippingOptions.map((cso) => cso.shipping_option_id) + } + + const rawOpts = await this.shippingOptionService_ + .withTransaction(manager) + .list(selector, { + relations: ["requirements", "profile"], + }) + + // if there are custom shipping options associated with the cart, return cart shipping options with custom price + if (hasCustomShippingOptions) { + return rawOpts.map((so) => { + const customOption = customShippingOptions.find( + (cso) => cso.shipping_option_id === so.id + ) + + return { + ...so, + amount: customOption?.price, + } + }) as ShippingOption[] + } + + const options = await Promise.all( + rawOpts.map(async (so) => { + try { + const option = await this.shippingOptionService_ + .withTransaction(manager) + .validateCartOption(so, cart) + if (option) { + return option + } + return null + } catch (err) { + // if validateCartOption fails it means the option is not valid + return null + } + }) + ) + + return options.filter(Boolean) as ShippingOption[] + }) + } + + /** + * Returns a list of all the productIds in the cart. + * @param cart - the cart to extract products from + * @return a list of product ids + */ + protected getProfilesInCart(cart: Cart): string[] { + return cart.items.reduce((acc, next) => { + // We may have line items that are not associated with a product + if (next.variant && next.variant.product) { + if (!acc.includes(next.variant.product.profile_id)) { + acc.push(next.variant.product.profile_id) + } + } + + return acc + }, [] as string[]) + } +} + +export default ShippingProfileService diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 5033c59612..d3175f2c6e 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -20,11 +20,10 @@ import { ClassConstructor } from "./global" /** * Utility type used to remove some optional attributes (coming from K) from a type T */ -export type WithRequiredProperty = T & - { - // -? removes 'optional' from a property - [Property in K]-?: T[Property] - } +export type WithRequiredProperty = T & { + // -? removes 'optional' from a property + [Property in K]-?: T[Property] +} export type PartialPick = { [P in K]?: T[P] @@ -80,10 +79,9 @@ export interface FindConfig { export interface CustomFindOptions { select?: FindManyOptions["select"] - where?: FindManyOptions["where"] & - { - [P in InKeys]?: TModel[P][] - } + where?: FindManyOptions["where"] & { + [P in InKeys]?: TModel[P][] + } order?: OrderByCondition skip?: number take?: number diff --git a/packages/medusa/src/types/shipping-options.ts b/packages/medusa/src/types/shipping-options.ts index 4b03a84168..18c7b8f673 100644 --- a/packages/medusa/src/types/shipping-options.ts +++ b/packages/medusa/src/types/shipping-options.ts @@ -69,5 +69,6 @@ export type UpdateShippingOptionInput = { requirements?: ShippingOptionRequirement[] region_id?: string provider_id?: string + profile_id?: string data?: string } diff --git a/packages/medusa/src/types/shipping-profile.ts b/packages/medusa/src/types/shipping-profile.ts new file mode 100644 index 0000000000..2a4db42f37 --- /dev/null +++ b/packages/medusa/src/types/shipping-profile.ts @@ -0,0 +1,13 @@ +import { Product, ShippingOption, ShippingProfileType } from "../models" + +export type CreateShippingProfile = { + name: string +} + +export type UpdateShippingProfile = { + name?: string + metadata?: Record + type?: ShippingProfileType + products?: string[] + shipping_options?: string[] +} From 4b663cca3acf43b0e02a1fb94b8d4f14913bfe45 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 8 Aug 2022 21:11:34 +0200 Subject: [PATCH 08/34] feat(medusa): Use transactions in CartCompletionStrategy (#1968) --- .changeset/tricky-suns-wink.md | 5 + .../api/routes/store/carts/complete-cart.ts | 6 +- .../interfaces/cart-completion-strategy.ts | 23 +- packages/medusa/src/loaders/plugins.ts | 17 + .../strategies/__tests__/cart-completion.js | 1 + .../medusa/src/strategies/cart-completion.ts | 436 ++++++++++-------- 6 files changed, 279 insertions(+), 209 deletions(-) create mode 100644 .changeset/tricky-suns-wink.md diff --git a/.changeset/tricky-suns-wink.md b/.changeset/tricky-suns-wink.md new file mode 100644 index 0000000000..ec435de4a2 --- /dev/null +++ b/.changeset/tricky-suns-wink.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Use transactions in CartCompletionStrategy phases diff --git a/packages/medusa/src/api/routes/store/carts/complete-cart.ts b/packages/medusa/src/api/routes/store/carts/complete-cart.ts index 8d422e486a..9362885efd 100644 --- a/packages/medusa/src/api/routes/store/carts/complete-cart.ts +++ b/packages/medusa/src/api/routes/store/carts/complete-cart.ts @@ -1,5 +1,5 @@ import { EntityManager } from "typeorm"; -import { ICartCompletionStrategy } from "../../../../interfaces" +import { AbstractCartCompletionStrategy } from "../../../../interfaces" import { IdempotencyKey } from "../../../../models/idempotency-key" import { IdempotencyKeyService } from "../../../../services" @@ -54,6 +54,7 @@ import { IdempotencyKeyService } from "../../../../services" export default async (req, res) => { const { id } = req.params + const manager: EntityManager = req.scope.resolve("manager") const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve( "idempotencyKeyService" ) @@ -62,7 +63,6 @@ export default async (req, res) => { let idempotencyKey: IdempotencyKey try { - const manager: EntityManager = req.scope.resolve("manager") idempotencyKey = await manager.transaction(async (transactionManager) => { return await idempotencyKeyService.withTransaction(transactionManager).initializeRequest( headerKey, @@ -80,7 +80,7 @@ export default async (req, res) => { res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key") res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key) - const completionStrat: ICartCompletionStrategy = req.scope.resolve( + const completionStrat: AbstractCartCompletionStrategy = req.scope.resolve( "cartCompletionStrategy" ) diff --git a/packages/medusa/src/interfaces/cart-completion-strategy.ts b/packages/medusa/src/interfaces/cart-completion-strategy.ts index f819a79c6d..c28dc849be 100644 --- a/packages/medusa/src/interfaces/cart-completion-strategy.ts +++ b/packages/medusa/src/interfaces/cart-completion-strategy.ts @@ -1,5 +1,6 @@ -import { IdempotencyKey } from "../models/idempotency-key" +import { IdempotencyKey } from "../models" import { RequestContext } from "../types/request" +import { TransactionBaseService } from "./transaction-base-service" export type CartCompletionResponse = { /** The response code for the completion request */ @@ -25,9 +26,19 @@ export interface ICartCompletionStrategy { ): Promise } -export function isCartCompletionStrategy( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - object: any -): object is ICartCompletionStrategy { - return typeof object.complete === "function" +export abstract class AbstractCartCompletionStrategy + implements ICartCompletionStrategy +{ + abstract complete( + cartId: string, + idempotencyKey: IdempotencyKey, + context: RequestContext + ): Promise +} + +export function isCartCompletionStrategy(obj: unknown): boolean { + return ( + typeof (obj as AbstractCartCompletionStrategy).complete === "function" || + obj instanceof AbstractCartCompletionStrategy + ) } diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 182c287f78..98ecaf8ad4 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -17,6 +17,7 @@ import { EntitySchema } from "typeorm" import { AbstractTaxService, isBatchJobStrategy, + isCartCompletionStrategy, isFileService, isNotificationService, isPriceSelectionStrategy, @@ -184,6 +185,22 @@ export function registerStrategies( break } + case isCartCompletionStrategy(module.prototype): { + if (!("cartCompletionStrategy" in registeredServices)) { + container.register({ + cartCompletionStrategy: asFunction( + (cradle) => new module(cradle, pluginDetails.options) + ).singleton(), + }) + registeredServices["cartCompletionStrategy"] = file + } else { + logger.warn( + `Cannot register ${file}. A cart completion strategy is already registered` + ) + } + break + } + case isBatchJobStrategy(module.prototype): { container.registerAdd( "batchJobStrategies", diff --git a/packages/medusa/src/strategies/__tests__/cart-completion.js b/packages/medusa/src/strategies/__tests__/cart-completion.js index 328ac2b1e9..492281b5e9 100644 --- a/packages/medusa/src/strategies/__tests__/cart-completion.js +++ b/packages/medusa/src/strategies/__tests__/cart-completion.js @@ -205,6 +205,7 @@ describe("CartCompletionStrategy", () => { idempotencyKeyService: idempotencyKeyServiceMock, orderService: orderServiceMock, swapService: swapServiceMock, + manager: MockManager }) const val = await completionStrat.complete(cart.id, idempotencyKey, {}) diff --git a/packages/medusa/src/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index 1ffc99a066..293ed4a6eb 100644 --- a/packages/medusa/src/strategies/cart-completion.ts +++ b/packages/medusa/src/strategies/cart-completion.ts @@ -1,32 +1,48 @@ import { EntityManager } from "typeorm" import { MedusaError } from "medusa-core-utils" -import { IdempotencyKey } from "../models/idempotency-key" -import { Order } from "../models/order" +import { IdempotencyKey, Order } from "../models" import CartService from "../services/cart" import { RequestContext } from "../types/request" import OrderService from "../services/order" import IdempotencyKeyService from "../services/idempotency-key" import SwapService from "../services/swap" -import { ICartCompletionStrategy, CartCompletionResponse } from "../interfaces" +import { + CartCompletionResponse, + AbstractCartCompletionStrategy, +} from "../interfaces" -class CartCompletionStrategy implements ICartCompletionStrategy { - private idempotencyKeyService_: IdempotencyKeyService - private cartService_: CartService - private orderService_: OrderService - private swapService_: SwapService +type InjectedDependencies = { + idempotencyKeyService: IdempotencyKeyService + cartService: CartService + orderService: OrderService + swapService: SwapService + manager: EntityManager +} + +class CartCompletionStrategy extends AbstractCartCompletionStrategy { + protected manager_: EntityManager + + protected readonly idempotencyKeyService_: IdempotencyKeyService + protected readonly cartService_: CartService + protected readonly orderService_: OrderService + protected readonly swapService_: SwapService constructor({ idempotencyKeyService, cartService, orderService, swapService, - }) { + manager, + }: InjectedDependencies) { + super() + this.idempotencyKeyService_ = idempotencyKeyService this.cartService_ = cartService this.orderService_ = orderService this.swapService_ = swapService + this.manager_ = manager } async complete( @@ -47,159 +63,211 @@ class CartCompletionStrategy implements ICartCompletionStrategy { while (inProgress) { switch (idempotencyKey.recovery_point) { case "started": { - const { key, error } = await idempotencyKeyService.workStage( - idempotencyKey.idempotency_key, - async (manager: EntityManager) => { - const cart = await cartService - .withTransaction(manager) - .retrieve(id) - - if (cart.completed_at) { - return { - response_code: 409, - response_body: { - code: MedusaError.Codes.CART_INCOMPATIBLE_STATE, - message: "Cart has already been completed", - type: MedusaError.Types.NOT_ALLOWED, - }, - } - } - - await cartService.withTransaction(manager).createTaxLines(id) - - return { - recovery_point: "tax_lines_created", - } - } - ) - - if (error) { - inProgress = false - err = error - } else { - idempotencyKey = key - } - break - } - case "tax_lines_created": { - const { key, error } = await idempotencyKeyService.workStage( - idempotencyKey.idempotency_key, - async (manager: EntityManager) => { - const cart = await cartService - .withTransaction(manager) - .authorizePayment(id, { - ...context, - idempotency_key: idempotencyKey.idempotency_key, - }) - - if (cart.payment_session) { - if ( - cart.payment_session.status === "requires_more" || - cart.payment_session.status === "pending" - ) { - return { - response_code: 200, - response_body: { - data: cart, - payment_status: cart.payment_session.status, - type: "cart", - }, - } - } - } - - return { - recovery_point: "payment_authorized", - } - } - ) - - if (error) { - inProgress = false - err = error - } else { - idempotencyKey = key - } - break - } - - case "payment_authorized": { - const { key, error } = await idempotencyKeyService.workStage( - idempotencyKey.idempotency_key, - async (manager: EntityManager) => { - const cart = await cartService - .withTransaction(manager) - .retrieve(id, { - select: ["total"], - relations: ["payment", "payment_sessions"], - }) - - // If cart is part of swap, we register swap as complete - switch (cart.type) { - case "swap": { - try { - const swapId = cart.metadata?.swap_id - let swap = await swapService - .withTransaction(manager) - .registerCartCompletion(swapId as string) - - swap = await swapService - .withTransaction(manager) - .retrieve(swap.id, { relations: ["shipping_address"] }) + await this.manager_.transaction(async (transactionManager) => { + const { key, error } = await idempotencyKeyService + .withTransaction(transactionManager) + .workStage( + idempotencyKey.idempotency_key, + async (manager: EntityManager) => { + const cart = await cartService + .withTransaction(manager) + .retrieve(id) + if (cart.completed_at) { return { - response_code: 200, - response_body: { data: swap, type: "swap" }, - } - } catch (error) { - if ( - error && - error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY - ) { - return { - response_code: 409, - response_body: { - message: error.message, - type: error.type, - code: error.code, - }, - } - } else { - throw error - } - } - } - // case "payment_link": - default: { - if (typeof cart.total === "undefined") { - return { - response_code: 500, + response_code: 409, response_body: { - message: "Unexpected state", + code: MedusaError.Codes.CART_INCOMPATIBLE_STATE, + message: "Cart has already been completed", + type: MedusaError.Types.NOT_ALLOWED, }, } } - if (!cart.payment && cart.total > 0) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Cart payment not authorized` - ) + await cartService.withTransaction(manager).createTaxLines(id) + + return { + recovery_point: "tax_lines_created", + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + }) + break + } + case "tax_lines_created": { + await this.manager_.transaction(async (transactionManager) => { + const { key, error } = await idempotencyKeyService + .withTransaction(transactionManager) + .workStage( + idempotencyKey.idempotency_key, + async (manager: EntityManager) => { + const cart = await cartService + .withTransaction(manager) + .authorizePayment(id, { + ...context, + idempotency_key: idempotencyKey.idempotency_key, + }) + + if (cart.payment_session) { + if ( + cart.payment_session.status === "requires_more" || + cart.payment_session.status === "pending" + ) { + return { + response_code: 200, + response_body: { + data: cart, + payment_status: cart.payment_session.status, + type: "cart", + }, + } + } } - let order: Order - try { - order = await orderService - .withTransaction(manager) - .createFromCart(cart.id) - } catch (error) { - if ( - error && - error.message === "Order from cart already exists" - ) { + return { + recovery_point: "payment_authorized", + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + }) + break + } + + case "payment_authorized": { + await this.manager_.transaction(async (transactionManager) => { + const { key, error } = await idempotencyKeyService + .withTransaction(transactionManager) + .workStage( + idempotencyKey.idempotency_key, + async (manager: EntityManager) => { + const cart = await cartService + .withTransaction(manager) + .retrieve(id, { + select: ["total"], + relations: ["payment", "payment_sessions"], + }) + + // If cart is part of swap, we register swap as complete + switch (cart.type) { + case "swap": { + try { + const swapId = cart.metadata?.swap_id + let swap = await swapService + .withTransaction(manager) + .registerCartCompletion(swapId as string) + + swap = await swapService + .withTransaction(manager) + .retrieve(swap.id, { + relations: ["shipping_address"], + }) + + return { + response_code: 200, + response_body: { data: swap, type: "swap" }, + } + } catch (error) { + if ( + error && + error.code === + MedusaError.Codes.INSUFFICIENT_INVENTORY + ) { + return { + response_code: 409, + response_body: { + message: error.message, + type: error.type, + code: error.code, + }, + } + } else { + throw error + } + } + } + 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, + `Cart payment not authorized` + ) + } + + let order: Order + try { + order = await orderService + .withTransaction(manager) + .createFromCart(cart.id) + } catch (error) { + if ( + error && + error.message === "Order from cart already exists" + ) { + order = await orderService + .withTransaction(manager) + .retrieveByCartId(id, { + select: [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "total", + ], + relations: [ + "shipping_address", + "items", + "payments", + ], + }) + + return { + response_code: 200, + response_body: { data: order, type: "order" }, + } + } else if ( + error && + error.code === + MedusaError.Codes.INSUFFICIENT_INVENTORY + ) { + return { + response_code: 409, + response_body: { + message: error.message, + type: error.type, + code: error.code, + }, + } + } else { + throw error + } + } + order = await orderService .withTransaction(manager) - .retrieveByCartId(id, { + .retrieve(order.id, { select: [ "subtotal", "tax_total", @@ -214,51 +282,18 @@ class CartCompletionStrategy implements ICartCompletionStrategy { response_code: 200, response_body: { data: order, type: "order" }, } - } else if ( - error && - error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY - ) { - return { - response_code: 409, - response_body: { - message: error.message, - type: error.type, - code: error.code, - }, - } - } else { - throw error } } - - order = await orderService - .withTransaction(manager) - .retrieve(order.id, { - select: [ - "subtotal", - "tax_total", - "shipping_total", - "discount_total", - "total", - ], - relations: ["shipping_address", "items", "payments"], - }) - - return { - response_code: 200, - response_body: { data: order, type: "order" }, - } } - } - } - ) + ) - if (error) { - inProgress = false - err = error - } else { - idempotencyKey = key - } + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + }) break } @@ -268,14 +303,15 @@ class CartCompletionStrategy implements ICartCompletionStrategy { } default: - idempotencyKey = await idempotencyKeyService.update( - idempotencyKey.idempotency_key, - { - recovery_point: "finished", - response_code: 500, - response_body: { message: "Unknown recovery point" }, - } - ) + await this.manager_.transaction(async (transactionManager) => { + idempotencyKey = await idempotencyKeyService + .withTransaction(transactionManager) + .update(idempotencyKey.idempotency_key, { + recovery_point: "finished", + response_code: 500, + response_body: { message: "Unknown recovery point" }, + }) + }) break } } From aaebb38eae883a225779b03556900ea813c991d2 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 9 Aug 2022 08:08:07 +0200 Subject: [PATCH 09/34] feat(medusa): Convert IdempotencyKeyService to TypeScript (#1995) * feat(medusa): Migrate the idempotency key service to ts + fix * feat(medusa): Finalise idempotency migration * Create late-owls-pump.md * feat(medusa): Polish * feat(medusa): Add case to the error handler * feat(medusa): Add case to the error handler Co-authored-by: olivermrbl --- .changeset/late-owls-pump.md | 6 + packages/medusa-core-utils/src/errors.ts | 1 + .../src/api/middlewares/error-handler.ts | 1 + .../api/routes/admin/orders/create-swap.ts | 2 +- .../api/routes/store/carts/calculate-taxes.ts | 5 +- .../api/routes/store/returns/create-return.ts | 2 +- .../src/api/routes/store/swaps/create-swap.ts | 2 +- .../medusa/src/services/idempotency-key.js | 173 --------------- .../medusa/src/services/idempotency-key.ts | 198 ++++++++++++++++++ .../medusa/src/strategies/cart-completion.ts | 16 +- packages/medusa/src/types/idempotency-key.ts | 6 + 11 files changed, 226 insertions(+), 186 deletions(-) create mode 100644 .changeset/late-owls-pump.md delete mode 100644 packages/medusa/src/services/idempotency-key.js create mode 100644 packages/medusa/src/services/idempotency-key.ts create mode 100644 packages/medusa/src/types/idempotency-key.ts diff --git a/.changeset/late-owls-pump.md b/.changeset/late-owls-pump.md new file mode 100644 index 0000000000..e9e2a1da11 --- /dev/null +++ b/.changeset/late-owls-pump.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +--- + +Convert IdempotencyKeyService to TypeScript +Add await to retrieve in lock method diff --git a/packages/medusa-core-utils/src/errors.ts b/packages/medusa-core-utils/src/errors.ts index fefe1536cd..aca37b9409 100644 --- a/packages/medusa-core-utils/src/errors.ts +++ b/packages/medusa-core-utils/src/errors.ts @@ -11,6 +11,7 @@ export const MedusaErrorTypes = { NOT_FOUND: "not_found", NOT_ALLOWED: "not_allowed", UNEXPECTED_STATE: "unexpected_state", + CONFLICT: "conflict", } export const MedusaErrorCodes = { diff --git a/packages/medusa/src/api/middlewares/error-handler.ts b/packages/medusa/src/api/middlewares/error-handler.ts index b4407a382f..6690665915 100644 --- a/packages/medusa/src/api/middlewares/error-handler.ts +++ b/packages/medusa/src/api/middlewares/error-handler.ts @@ -33,6 +33,7 @@ export default () => { case QUERY_RUNNER_RELEASED: case TRANSACTION_STARTED: case TRANSACTION_NOT_STARTED: + case MedusaError.Types.CONFLICT: statusCode = 409 errObj.code = INVALID_STATE_ERROR errObj.message = diff --git a/packages/medusa/src/api/routes/admin/orders/create-swap.ts b/packages/medusa/src/api/routes/admin/orders/create-swap.ts index 10fd88e34f..b7878ec3cc 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-swap.ts +++ b/packages/medusa/src/api/routes/admin/orders/create-swap.ts @@ -148,7 +148,7 @@ export default async (req, res) => { res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key) let inProgress = true - let err = false + let err: unknown = false while (inProgress) { switch (idempotencyKey.recovery_point) { diff --git a/packages/medusa/src/api/routes/store/carts/calculate-taxes.ts b/packages/medusa/src/api/routes/store/carts/calculate-taxes.ts index aa3e957eb6..64e79befe3 100644 --- a/packages/medusa/src/api/routes/store/carts/calculate-taxes.ts +++ b/packages/medusa/src/api/routes/store/carts/calculate-taxes.ts @@ -34,7 +34,8 @@ export default async (req, res) => { const headerKey = req.get("Idempotency-Key") || "" - let idempotencyKey!: IdempotencyKey + let idempotencyKey + try { await manager.transaction(async (transactionManager) => { idempotencyKey = await idempotencyKeyService @@ -58,7 +59,7 @@ export default async (req, res) => { const cartService: CartService = req.scope.resolve("cartService") let inProgress = true - let err = false + let err: unknown = false while (inProgress) { switch (idempotencyKey.recovery_point) { diff --git a/packages/medusa/src/api/routes/store/returns/create-return.ts b/packages/medusa/src/api/routes/store/returns/create-return.ts index 600c802f76..50229128d9 100644 --- a/packages/medusa/src/api/routes/store/returns/create-return.ts +++ b/packages/medusa/src/api/routes/store/returns/create-return.ts @@ -108,7 +108,7 @@ export default async (req, res) => { const eventBus: EventBusService = req.scope.resolve("eventBusService") let inProgress = true - let err = false + let err: unknown = false while (inProgress) { switch (idempotencyKey.recovery_point) { diff --git a/packages/medusa/src/api/routes/store/swaps/create-swap.ts b/packages/medusa/src/api/routes/store/swaps/create-swap.ts index 1877753b49..c5a261da28 100644 --- a/packages/medusa/src/api/routes/store/swaps/create-swap.ts +++ b/packages/medusa/src/api/routes/store/swaps/create-swap.ts @@ -114,7 +114,7 @@ export default async (req, res) => { const returnService: ReturnService = req.scope.resolve("returnService") let inProgress = true - let err = false + let err: unknown = false while (inProgress) { switch (idempotencyKey.recovery_point) { diff --git a/packages/medusa/src/services/idempotency-key.js b/packages/medusa/src/services/idempotency-key.js deleted file mode 100644 index 175124e3aa..0000000000 --- a/packages/medusa/src/services/idempotency-key.js +++ /dev/null @@ -1,173 +0,0 @@ -import { MedusaError } from "medusa-core-utils" -import { v4 } from "uuid" -import { TransactionBaseService } from "../interfaces" - -const KEY_LOCKED_TIMEOUT = 1000 - -class IdempotencyKeyService extends TransactionBaseService { - constructor({ manager, idempotencyKeyRepository }) { - super({ manager, idempotencyKeyRepository }) - - /** @private @constant {EntityManager} */ - this.manager_ = manager - - /** @private @constant {IdempotencyKeyRepository} */ - this.idempotencyKeyRepository_ = idempotencyKeyRepository - } - - /** - * Execute the initial steps in a idempotent request. - * @param {string} headerKey - potential idempotency key from header - * @param {string} reqMethod - method of request - * @param {string} reqParams - params of request - * @param {string} reqPath - path of request - * @return {Promise} the existing or created idempotency key - */ - async initializeRequest(headerKey, reqMethod, reqParams, reqPath) { - return this.atomicPhase_(async (_) => { - // If idempotency key exists, return it - let key = await this.retrieve(headerKey) - - if (key) { - return key - } - - key = await this.create({ - request_method: reqMethod, - request_params: reqParams, - request_path: reqPath, - }) - - return key - }, "SERIALIZABLE") - } - - /** - * Creates an idempotency key for a request. - * If no idempotency key is provided in request, we will create a unique - * identifier. - * @param {object} payload - payload of request to create idempotency key for - * @return {Promise} the created idempotency key - */ - async create(payload) { - return this.atomicPhase_(async (manager) => { - const idempotencyKeyRepo = manager.getCustomRepository( - this.idempotencyKeyRepository_ - ) - - if (!payload.idempotency_key) { - payload.idempotency_key = v4() - } - - const created = await idempotencyKeyRepo.create(payload) - const result = await idempotencyKeyRepo.save(created) - return result - }) - } - - /** - * Retrieves an idempotency key - * @param {string} idempotencyKey - key to retrieve - * @return {Promise} idempotency key - */ - async retrieve(idempotencyKey) { - const idempotencyKeyRepo = this.manager_.getCustomRepository( - this.idempotencyKeyRepository_ - ) - - const key = await idempotencyKeyRepo.findOne({ - where: { idempotency_key: idempotencyKey }, - }) - - return key - } - - /** - * Locks an idempotency. - * @param {string} idempotencyKey - key to lock - * @return {Promise} result of the update operation - */ - async lock(idempotencyKey) { - return this.atomicPhase_(async (manager) => { - const idempotencyKeyRepo = manager.getCustomRepository( - this.idempotencyKeyRepository_ - ) - - const key = this.retrieve(idempotencyKey) - - if (key.locked_at && key.locked_at > Date.now() - KEY_LOCKED_TIMEOUT) { - throw new MedusaError("conflict", "Key already locked") - } - - const updated = await idempotencyKeyRepo.save({ - ...key, - locked_at: Date.now(), - }) - - return updated - }) - } - - /** - * Locks an idempotency. - * @param {string} idempotencyKey - key to update - * @param {object} update - update object - * @return {Promise} result of the update operation - */ - async update(idempotencyKey, update) { - return this.atomicPhase_(async (manager) => { - const idempotencyKeyRepo = manager.getCustomRepository( - this.idempotencyKeyRepository_ - ) - - const iKey = await this.retrieve(idempotencyKey) - - for (const [key, value] of Object.entries(update)) { - iKey[key] = value - } - - const updated = await idempotencyKeyRepo.save(iKey) - return updated - }) - } - - /** - * Performs an atomic work stage. - * An atomic work stage contains some related functionality, that needs to be - * transactionally executed in isolation. An idempotent request will - * always consist of 2 or more of these phases. The required phases are - * "started" and "finished". - * @param {string} idempotencyKey - current idempotency key - * @param {Function} func - functionality to execute within the phase - * @return {IdempotencyKeyModel} new updated idempotency key - */ - async workStage(idempotencyKey, func) { - try { - return await this.atomicPhase_(async (manager) => { - let key - - const { recovery_point, response_code, response_body } = await func( - manager - ) - - if (recovery_point) { - key = await this.update(idempotencyKey, { - recovery_point, - }) - } else { - key = await this.update(idempotencyKey, { - recovery_point: "finished", - response_body, - response_code, - }) - } - - return { key } - }, "SERIALIZABLE") - } catch (err) { - return { error: err } - } - } -} - -export default IdempotencyKeyService diff --git a/packages/medusa/src/services/idempotency-key.ts b/packages/medusa/src/services/idempotency-key.ts new file mode 100644 index 0000000000..85026a1055 --- /dev/null +++ b/packages/medusa/src/services/idempotency-key.ts @@ -0,0 +1,198 @@ +import { MedusaError } from "medusa-core-utils" +import { v4 } from "uuid" +import { TransactionBaseService } from "../interfaces" +import { DeepPartial, EntityManager } from "typeorm" +import { IdempotencyKeyRepository } from "../repositories/idempotency-key" +import { IdempotencyKey } from "../models" +import { CreateIdempotencyKeyInput } from "../types/idempotency-key" + +const KEY_LOCKED_TIMEOUT = 1000 + +type InjectedDependencies = { + manager: EntityManager + idempotencyKeyRepository: typeof IdempotencyKeyRepository +} + +class IdempotencyKeyService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly idempotencyKeyRepository_: typeof IdempotencyKeyRepository + + constructor({ manager, idempotencyKeyRepository }: InjectedDependencies) { + super({ manager, idempotencyKeyRepository }) + + this.manager_ = manager + this.idempotencyKeyRepository_ = idempotencyKeyRepository + } + + /** + * Execute the initial steps in a idempotent request. + * @param headerKey - potential idempotency key from header + * @param reqMethod - method of request + * @param reqParams - params of request + * @param reqPath - path of request + * @return the existing or created idempotency key + */ + async initializeRequest( + headerKey: string, + reqMethod: string, + reqParams: Record, + reqPath: string + ): Promise { + return await this.atomicPhase_(async () => { + const key = await this.retrieve(headerKey).catch(() => void 0) + if (key) { + return key + } + return await this.create({ + request_method: reqMethod, + request_params: reqParams, + request_path: reqPath, + }) + }, "SERIALIZABLE") + } + + /** + * Creates an idempotency key for a request. + * If no idempotency key is provided in request, we will create a unique + * identifier. + * @param payload - payload of request to create idempotency key for + * @return the created idempotency key + */ + async create(payload: CreateIdempotencyKeyInput): Promise { + return await this.atomicPhase_(async (manager) => { + const idempotencyKeyRepo = manager.getCustomRepository( + this.idempotencyKeyRepository_ + ) + + payload.idempotency_key = payload.idempotency_key ?? v4() + + const created = idempotencyKeyRepo.create(payload) + return await idempotencyKeyRepo.save(created) + }) + } + + /** + * Retrieves an idempotency key + * @param idempotencyKey - key to retrieve + * @return idempotency key + */ + async retrieve(idempotencyKey: string): Promise { + const idempotencyKeyRepo = this.manager_.getCustomRepository( + this.idempotencyKeyRepository_ + ) + + const iKey = await idempotencyKeyRepo.findOne({ + where: { idempotency_key: idempotencyKey }, + }) + + if (!iKey) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Idempotency key ${idempotencyKey} was not found` + ) + } + + return iKey + } + + /** + * Locks an idempotency. + * @param idempotencyKey - key to lock + * @return result of the update operation + */ + async lock(idempotencyKey: string): Promise { + return await this.atomicPhase_(async (manager) => { + const idempotencyKeyRepo = manager.getCustomRepository( + this.idempotencyKeyRepository_ + ) + + const key = await this.retrieve(idempotencyKey) + + const isLocked = + key.locked_at && + new Date(key.locked_at).getTime() > Date.now() - KEY_LOCKED_TIMEOUT + + if (isLocked) { + throw new MedusaError(MedusaError.Types.CONFLICT, "Key already locked") + } + + return await idempotencyKeyRepo.save({ + ...key, + locked_at: Date.now(), + }) + }) + } + + /** + * Locks an idempotency. + * @param {string} idempotencyKey - key to update + * @param {object} update - update object + * @return {Promise} result of the update operation + */ + async update( + idempotencyKey: string, + update: DeepPartial + ): Promise { + return await this.atomicPhase_(async (manager) => { + const idempotencyKeyRepo = manager.getCustomRepository( + this.idempotencyKeyRepository_ + ) + + const iKey = await this.retrieve(idempotencyKey) + + for (const [key, value] of Object.entries(update)) { + iKey[key] = value + } + + return await idempotencyKeyRepo.save(iKey) + }) + } + + /** + * Performs an atomic work stage. + * An atomic work stage contains some related functionality, that needs to be + * transactionally executed in isolation. An idempotent request will + * always consist of 2 or more of these phases. The required phases are + * "started" and "finished". + * @param idempotencyKey - current idempotency key + * @param callback - functionality to execute within the phase + * @return new updated idempotency key + */ + async workStage( + idempotencyKey: string, + callback: (transactionManager: EntityManager) => Promise< + | { + recovery_point?: string + response_code?: number + response_body?: Record + } + | never + > + ): Promise<{ key?: IdempotencyKey; error?: unknown }> { + try { + return await this.atomicPhase_(async (manager) => { + const { recovery_point, response_code, response_body } = await callback( + manager + ) + + const data: DeepPartial = { + recovery_point: recovery_point ?? "finished", + } + + if (!recovery_point) { + data.response_body = response_body + data.response_code = response_code + } + + const key = await this.update(idempotencyKey, data) + return { key } + }, "SERIALIZABLE") + } catch (err) { + return { error: err } + } + } +} + +export default IdempotencyKeyService diff --git a/packages/medusa/src/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index 293ed4a6eb..5f50255932 100644 --- a/packages/medusa/src/strategies/cart-completion.ts +++ b/packages/medusa/src/strategies/cart-completion.ts @@ -1,16 +1,16 @@ -import { EntityManager } from "typeorm" import { MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" import { IdempotencyKey, Order } from "../models" import CartService from "../services/cart" -import { RequestContext } from "../types/request" -import OrderService from "../services/order" import IdempotencyKeyService from "../services/idempotency-key" +import OrderService from "../services/order" import SwapService from "../services/swap" +import { RequestContext } from "../types/request" import { - CartCompletionResponse, AbstractCartCompletionStrategy, + CartCompletionResponse, } from "../interfaces" type InjectedDependencies = { @@ -58,7 +58,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { const swapService = this.swapService_ let inProgress = true - let err = false + let err: unknown = false while (inProgress) { switch (idempotencyKey.recovery_point) { @@ -96,7 +96,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { inProgress = false err = error } else { - idempotencyKey = key + idempotencyKey = key as IdempotencyKey } }) break @@ -141,7 +141,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { inProgress = false err = error } else { - idempotencyKey = key + idempotencyKey = key as IdempotencyKey } }) break @@ -291,7 +291,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { inProgress = false err = error } else { - idempotencyKey = key + idempotencyKey = key as IdempotencyKey } }) break diff --git a/packages/medusa/src/types/idempotency-key.ts b/packages/medusa/src/types/idempotency-key.ts new file mode 100644 index 0000000000..5459bac4fe --- /dev/null +++ b/packages/medusa/src/types/idempotency-key.ts @@ -0,0 +1,6 @@ +export type CreateIdempotencyKeyInput = { + request_method: string + request_params: Record + request_path: string + idempotency_key?: string +} From 900260c5b9df4f4f927db5bb6921e5e139ff269a Mon Sep 17 00:00:00 2001 From: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:27:12 +0200 Subject: [PATCH 10/34] feat(medusa,medusa-telemetry): Add telemetry on feature flags (#2017) --- .changeset/eighty-onions-share.md | 6 +++ packages/medusa-telemetry/src/index.js | 4 ++ packages/medusa-telemetry/src/telemeter.js | 17 +++++-- .../medusa/src/loaders/feature-flags/index.ts | 9 +++- packages/medusa/src/loaders/index.ts | 46 ++++++++++--------- 5 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 .changeset/eighty-onions-share.md diff --git a/.changeset/eighty-onions-share.md b/.changeset/eighty-onions-share.md new file mode 100644 index 0000000000..373a03fc56 --- /dev/null +++ b/.changeset/eighty-onions-share.md @@ -0,0 +1,6 @@ +--- +"medusa-telemetry": patch +"@medusajs/medusa": patch +--- + +Adds enabled features flags to tracking event in `medusa-telemetry` diff --git a/packages/medusa-telemetry/src/index.js b/packages/medusa-telemetry/src/index.js index 2d4e355849..761137885a 100644 --- a/packages/medusa-telemetry/src/index.js +++ b/packages/medusa-telemetry/src/index.js @@ -17,4 +17,8 @@ export const setTelemetryEnabled = (enabled = true) => { telemeter.setTelemetryEnabled(enabled) } +export function trackFeatureFlag(flag) { + telemeter.trackFeatureFlag(flag) +} + export { default as Telemeter } from "./telemeter" diff --git a/packages/medusa-telemetry/src/telemeter.js b/packages/medusa-telemetry/src/telemeter.js index 28dd796bb9..8da93594ac 100644 --- a/packages/medusa-telemetry/src/telemeter.js +++ b/packages/medusa-telemetry/src/telemeter.js @@ -1,15 +1,15 @@ -import os from "os" import fs from "fs" -import { join, sep } from "path" import isDocker from "is-docker" +import os from "os" +import { join, sep } from "path" import { v4 as uuidv4 } from "uuid" +import Store from "./store" import createFlush from "./util/create-flush" import getTermProgram from "./util/get-term-program" +import { getCIName, isCI } from "./util/is-ci" import isTruthy from "./util/is-truthy" import showAnalyticsNotification from "./util/show-notification" -import { isCI, getCIName } from "./util/is-ci" -import Store from "./store" const MEDUSA_TELEMETRY_VERBOSE = process.env.MEDUSA_TELEMETRY_VERBOSE || false @@ -24,6 +24,8 @@ class Telemeter { this.queueSize_ = this.store_.getQueueSize() this.queueCount_ = this.store_.getQueueCount() + + this.featureFlags_ = new Set() } getMachineId() { @@ -130,6 +132,7 @@ class Telemeter { os_info: this.getOsInfo(), medusa_version: this.getMedusaVersion(), cli_version: this.getCliVersion(), + feature_flags: Array.from(this.featureFlags_), } this.store_.addEvent(event) @@ -152,6 +155,12 @@ class Telemeter { } } } + + trackFeatureFlag(flag) { + if (flag) { + this.featureFlags_.add(flag) + } + } } export default Telemeter diff --git a/packages/medusa/src/loaders/feature-flags/index.ts b/packages/medusa/src/loaders/feature-flags/index.ts index 368365da26..c76bcb4f25 100644 --- a/packages/medusa/src/loaders/feature-flags/index.ts +++ b/packages/medusa/src/loaders/feature-flags/index.ts @@ -1,9 +1,10 @@ -import path from "path" import glob from "glob" +import path from "path" +import { trackFeatureFlag } from "medusa-telemetry" import { FlagSettings } from "../../types/feature-flags" -import { FlagRouter } from "../../utils/flag-router" import { Logger } from "../../types/global" +import { FlagRouter } from "../../utils/flag-router" const isTruthy = (val: string | boolean | undefined): boolean => { if (typeof val === "string") { @@ -62,6 +63,10 @@ export default ( default: flagConfig[flagSettings.key] = flagSettings.default_val } + + if (flagConfig[flagSettings.key]) { + trackFeatureFlag(flagSettings.key) + } } return new FlagRouter(flagConfig) diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 95aea94980..35d0bd2389 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -1,24 +1,3 @@ -import loadConfig from "./config" -import "reflect-metadata" -import Logger from "./logger" -import apiLoader from "./api" -import featureFlagsLoader from "./feature-flags" -import databaseLoader from "./database" -import defaultsLoader from "./defaults" -import expressLoader from "./express" -import modelsLoader from "./models" -import passportLoader from "./passport" -import pluginsLoader, { registerPluginModels } from "./plugins" -import redisLoader from "./redis" -import repositoriesLoader from "./repositories" -import requestIp from "request-ip" -import searchIndexLoader from "./search-index" -import servicesLoader from "./services" -import strategiesLoader from "./strategies" -import subscribersLoader from "./subscribers" -import { ClassOrFunctionReturning } from "awilix/lib/container" -import { Connection, getManager } from "typeorm" -import { Express, NextFunction, Request, Response } from "express" import { asFunction, asValue, @@ -26,8 +5,29 @@ import { createContainer, Resolver, } from "awilix" +import { ClassOrFunctionReturning } from "awilix/lib/container" +import { Express, NextFunction, Request, Response } from "express" import { track } from "medusa-telemetry" +import "reflect-metadata" +import requestIp from "request-ip" +import { Connection, getManager } from "typeorm" import { MedusaContainer } from "../types/global" +import apiLoader from "./api" +import loadConfig from "./config" +import databaseLoader from "./database" +import defaultsLoader from "./defaults" +import expressLoader from "./express" +import featureFlagsLoader from "./feature-flags" +import Logger from "./logger" +import modelsLoader from "./models" +import passportLoader from "./passport" +import pluginsLoader, { registerPluginModels } from "./plugins" +import redisLoader from "./redis" +import repositoriesLoader from "./repositories" +import searchIndexLoader from "./search-index" +import servicesLoader from "./services" +import strategiesLoader from "./strategies" +import subscribersLoader from "./subscribers" type Options = { directory: string @@ -82,6 +82,7 @@ export default async ({ }) const featureFlagRouter = featureFlagsLoader(configModule, Logger) + track("FEATURE_FLAGS_LOADED") container.register({ logger: asValue(Logger), @@ -179,7 +180,8 @@ export default async ({ const searchActivity = Logger.activity("Initializing search engine indexing") track("SEARCH_ENGINE_INDEXING_STARTED") await searchIndexLoader({ container }) - const searchAct = Logger.success(searchActivity, "Indexing event emitted") || {} + const searchAct = + Logger.success(searchActivity, "Indexing event emitted") || {} track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) return { container, dbConnection, app: expressApp } From 987ce2ab6d6b3e47ba0ab2a8e629e92182023e9c Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 10 Aug 2022 13:02:26 +0200 Subject: [PATCH 11/34] chore(medusa): Feature flag loader simplify, deduplicate and increase readability (#2025) --- .../medusa/src/loaders/feature-flags/index.ts | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/packages/medusa/src/loaders/feature-flags/index.ts b/packages/medusa/src/loaders/feature-flags/index.ts index c76bcb4f25..5343019bd7 100644 --- a/packages/medusa/src/loaders/feature-flags/index.ts +++ b/packages/medusa/src/loaders/feature-flags/index.ts @@ -28,40 +28,30 @@ export default ( const flagConfig: Record = {} for (const flag of supportedFlags) { // eslint-disable-next-line @typescript-eslint/no-var-requires - const importedModule = require(flag) - if (!importedModule.default) { + const flagSettings: FlagSettings = require(flag).default + if (!flagSettings) { continue } - const flagSettings: FlagSettings = importedModule.default + flagConfig[flagSettings.key] = isTruthy(flagSettings.default_val) - switch (true) { - case typeof process.env[flagSettings.env_key] !== "undefined": - if (logger) { - logger.info( - `Using flag ${flagSettings.env_key} from environment with value ${ - process.env[flagSettings.env_key] - }` - ) - } - flagConfig[flagSettings.key] = isTruthy( - process.env[flagSettings.env_key] - ) - break - case typeof projectConfigFlags[flagSettings.key] !== "undefined": - if (logger) { - logger.info( - `Using flag ${flagSettings.key} from project config with value ${ - projectConfigFlags[flagSettings.key] - }` - ) - } - flagConfig[flagSettings.key] = isTruthy( - projectConfigFlags[flagSettings.key] - ) - break - default: - flagConfig[flagSettings.key] = flagSettings.default_val + let from + if (typeof process.env[flagSettings.env_key] !== "undefined") { + from = "environment" + flagConfig[flagSettings.key] = isTruthy(process.env[flagSettings.env_key]) + } else if (typeof projectConfigFlags[flagSettings.key] !== "undefined") { + from = "project config" + flagConfig[flagSettings.key] = isTruthy( + projectConfigFlags[flagSettings.key] + ) + } + + if (logger && from) { + logger.info( + `Using flag ${flagSettings.env_key} from ${from} with value ${ + flagConfig[flagSettings.key] + }` + ) } if (flagConfig[flagSettings.key]) { From bd031ef7adbb25152f5208f02ba0d7814a4f1a4b Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 10 Aug 2022 17:26:16 +0200 Subject: [PATCH 12/34] feat(medusa:) Convert PaymentProvider + PaymentProviderInterface to TS + (#1773) * feat(payments): Refactor core Payment related * fix(medusa): typings * test(unit): fix suite * test(unit): fix suite * feat(medusa): Improve payment provider container typings * fix(medusa): typings * styles(medusa): renove comments * feat(medusa): cleanup * feat(medusa): Add uniq constraint on payment session and idem key on create-payment-session end point * fix(medusa): migration * fix(medusa): create payment session * feat(medusa): cleanup --- .../fixture-gen/src/services/test-pay.js | 8 +- .../payment/how-to-create-payment-provider.md | 4 +- .../api/__tests__/taxes/manual-taxes.js | 2 - .../api/src/services/test-pay.js | 8 +- .../plugins/src/services/test-pay.js | 8 +- packages/medusa-interfaces/src/index.js | 4 +- .../store/carts/create-payment-sessions.ts | 101 +++- .../carts/decorate-line-items-with-totals.ts | 21 +- .../store/carts/update-payment-session.ts | 2 +- packages/medusa/src/interfaces/index.ts | 1 + .../medusa/src/interfaces/payment-service.ts | 116 ++++ packages/medusa/src/loaders/defaults.ts | 72 ++- packages/medusa/src/loaders/plugins.ts | 4 +- ...-payment_session_uniq_cartId_providerId.ts | 13 + packages/medusa/src/models/payment-session.ts | 1 + packages/medusa/src/models/payment.ts | 4 +- .../medusa/src/services/__mocks__/test-pay.js | 43 ++ .../medusa/src/services/__tests__/cart.js | 2 + .../services/__tests__/payment-provider.js | 189 +++++- .../medusa/src/services/__tests__/swap.js | 1 - packages/medusa/src/services/cart.ts | 16 +- packages/medusa/src/services/order.ts | 3 +- .../medusa/src/services/payment-provider.js | 438 -------------- .../medusa/src/services/payment-provider.ts | 544 ++++++++++++++++++ .../src/services/system-payment-provider.js | 6 +- packages/medusa/tsconfig.json | 7 +- 26 files changed, 1106 insertions(+), 512 deletions(-) create mode 100644 packages/medusa/src/interfaces/payment-service.ts create mode 100644 packages/medusa/src/migrations/1660040729000-payment_session_uniq_cartId_providerId.ts create mode 100644 packages/medusa/src/services/__mocks__/test-pay.js delete mode 100644 packages/medusa/src/services/payment-provider.js create mode 100644 packages/medusa/src/services/payment-provider.ts diff --git a/docs-util/fixture-gen/src/services/test-pay.js b/docs-util/fixture-gen/src/services/test-pay.js index 481261b3a0..7f5c3a0781 100644 --- a/docs-util/fixture-gen/src/services/test-pay.js +++ b/docs-util/fixture-gen/src/services/test-pay.js @@ -1,10 +1,10 @@ -import { PaymentService } from "medusa-interfaces"; +import { AbstractPaymentService } from "@medusajs/medusa"; -class TestPayService extends PaymentService { +class TestPayService extends AbstractPaymentService { static identifier = "test-pay"; - constructor() { - super(); + constructor(_) { + super(_); } async getStatus(paymentData) { diff --git a/docs/content/advanced/backend/payment/how-to-create-payment-provider.md b/docs/content/advanced/backend/payment/how-to-create-payment-provider.md index e867ffe91c..2c781c854a 100644 --- a/docs/content/advanced/backend/payment/how-to-create-payment-provider.md +++ b/docs/content/advanced/backend/payment/how-to-create-payment-provider.md @@ -47,9 +47,9 @@ These methods are used at different points in the Checkout flow as well as when The first step to create a payment provider is to create a file in `src/services` with the following content: ```jsx -import { PaymentService } from "medusa-interfaces" +import { AbstractPaymentService } from "@medusajs/medusa" -class MyPaymentService extends PaymentService { +class MyPaymentService extends AbstractPaymentService { } diff --git a/integration-tests/api/__tests__/taxes/manual-taxes.js b/integration-tests/api/__tests__/taxes/manual-taxes.js index 79c53033a5..89831bc9b6 100644 --- a/integration-tests/api/__tests__/taxes/manual-taxes.js +++ b/integration-tests/api/__tests__/taxes/manual-taxes.js @@ -6,8 +6,6 @@ const { initDb, useDb } = require("../../../helpers/use-db") const { simpleProductTaxRateFactory, - simpleShippingTaxRateFactory, - simpleShippingOptionFactory, simpleCartFactory, simpleRegionFactory, simpleProductFactory, diff --git a/integration-tests/api/src/services/test-pay.js b/integration-tests/api/src/services/test-pay.js index eed8aa80e5..eb202c01c2 100644 --- a/integration-tests/api/src/services/test-pay.js +++ b/integration-tests/api/src/services/test-pay.js @@ -1,10 +1,10 @@ -import { PaymentService } from "medusa-interfaces" +import { AbstractPaymentService } from "@medusajs/medusa" -class TestPayService extends PaymentService { +class TestPayService extends AbstractPaymentService { static identifier = "test-pay" - constructor() { - super() + constructor(_) { + super(_) } async getStatus(paymentData) { diff --git a/integration-tests/plugins/src/services/test-pay.js b/integration-tests/plugins/src/services/test-pay.js index eed8aa80e5..eb202c01c2 100644 --- a/integration-tests/plugins/src/services/test-pay.js +++ b/integration-tests/plugins/src/services/test-pay.js @@ -1,10 +1,10 @@ -import { PaymentService } from "medusa-interfaces" +import { AbstractPaymentService } from "@medusajs/medusa" -class TestPayService extends PaymentService { +class TestPayService extends AbstractPaymentService { static identifier = "test-pay" - constructor() { - super() + constructor(_) { + super(_) } async getStatus(paymentData) { diff --git a/packages/medusa-interfaces/src/index.js b/packages/medusa-interfaces/src/index.js index 10cf36af65..9cf865cf57 100644 --- a/packages/medusa-interfaces/src/index.js +++ b/packages/medusa-interfaces/src/index.js @@ -1,7 +1,7 @@ export { default as BaseService } from "./base-service" -export { default as FileService } from "./file-service" +export { default as PaymentService } from "./payment-service" export { default as FulfillmentService } from "./fulfillment-service" +export { default as FileService } from "./file-service" export { default as NotificationService } from "./notification-service" export { default as OauthService } from "./oauth-service" -export { default as PaymentService } from "./payment-service" export { default as SearchService } from "./search-service" diff --git a/packages/medusa/src/api/routes/store/carts/create-payment-sessions.ts b/packages/medusa/src/api/routes/store/carts/create-payment-sessions.ts index 15d5a8dbac..537268ab2e 100644 --- a/packages/medusa/src/api/routes/store/carts/create-payment-sessions.ts +++ b/packages/medusa/src/api/routes/store/carts/create-payment-sessions.ts @@ -2,6 +2,7 @@ import { defaultStoreCartFields, defaultStoreCartRelations } from "." import { CartService } from "../../../../services" import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals" import { EntityManager } from "typeorm"; +import IdempotencyKeyService from "../../../../services/idempotency-key"; /** * @oas [post] /carts/{id}/payment-sessions @@ -26,17 +27,97 @@ export default async (req, res) => { const { id } = req.params const cartService: CartService = req.scope.resolve("cartService") - + const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve( + "idempotencyKeyService" + ) const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await cartService.withTransaction(transactionManager).setPaymentSessions(id) - }) - const cart = await cartService.retrieve(id, { - select: defaultStoreCartFields, - relations: defaultStoreCartRelations, - }) + const headerKey = req.get("Idempotency-Key") || "" - const data = await decorateLineItemsWithTotals(cart, req) - res.status(200).json({ cart: data }) + let idempotencyKey + try { + await manager.transaction(async (transactionManager) => { + idempotencyKey = await idempotencyKeyService.withTransaction(transactionManager).initializeRequest( + headerKey, + req.method, + req.params, + req.path + ) + }) + } catch (error) { + res.status(409).send("Failed to create idempotency key") + return + } + + res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key") + res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key) + + try { + let inProgress = true + let err: unknown = false + + while (inProgress) { + switch (idempotencyKey.recovery_point) { + case "started": { + await manager.transaction(async (transactionManager) => { + const { key, error } = await idempotencyKeyService + .withTransaction(transactionManager) + .workStage( + idempotencyKey.idempotency_key, + async (stageManager) => { + await cartService.withTransaction(stageManager).setPaymentSessions(id) + + const cart = await cartService.withTransaction(stageManager).retrieve(id, { + select: defaultStoreCartFields, + relations: defaultStoreCartRelations, + }) + + const data = await decorateLineItemsWithTotals(cart, req, { + force_taxes: false, + transactionManager: stageManager + }) + + return { + response_code: 200, + response_body: { cart: data }, + } + }) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + }) + break + } + + case "finished": { + inProgress = false + break + } + + default: + await manager.transaction(async (transactionManager) => { + idempotencyKey = await idempotencyKeyService + .withTransaction(transactionManager) + .update( + idempotencyKey.idempotency_key, + { + recovery_point: "finished", + response_code: 500, + response_body: { message: "Unknown recovery point" }, + } + ) + }) + break + } + } + + res.status(idempotencyKey.response_code).json(idempotencyKey.response_body) + } catch (e) { + console.log(e) + throw e + } } diff --git a/packages/medusa/src/api/routes/store/carts/decorate-line-items-with-totals.ts b/packages/medusa/src/api/routes/store/carts/decorate-line-items-with-totals.ts index 8707770db8..ffea29618f 100644 --- a/packages/medusa/src/api/routes/store/carts/decorate-line-items-with-totals.ts +++ b/packages/medusa/src/api/routes/store/carts/decorate-line-items-with-totals.ts @@ -6,17 +6,16 @@ import { EntityManager } from "typeorm"; export const decorateLineItemsWithTotals = async ( cart: Cart, req: Request, - options: { force_taxes: boolean } = { force_taxes: false } + options: { force_taxes: boolean, transactionManager?: EntityManager } = { force_taxes: false } ): Promise => { const totalsService: TotalsService = req.scope.resolve("totalsService") if (cart.items && cart.region) { - const manager: EntityManager = req.scope.resolve("manager") - const items = await manager.transaction(async (transactionManager) => { + const getItems = async (manager) => { + const totalsServiceTx = totalsService.withTransaction(manager) return await Promise.all( cart.items.map(async (item: LineItem) => { - const itemTotals = await totalsService - .withTransaction(transactionManager) + const itemTotals = await totalsServiceTx .getLineItemTotals(item, cart, { include_tax: options.force_taxes || cart.region.automatic_taxes, }) @@ -24,7 +23,17 @@ export const decorateLineItemsWithTotals = async ( return Object.assign(item, itemTotals) }) ) - }) + } + + let items + if (options.transactionManager) { + items = await getItems(options.transactionManager) + } else { + const manager: EntityManager = options.transactionManager ?? req.scope.resolve("manager") + items = await manager.transaction(async (transactionManager) => { + return await getItems(transactionManager) + }) + } return Object.assign(cart, { items }) } diff --git a/packages/medusa/src/api/routes/store/carts/update-payment-session.ts b/packages/medusa/src/api/routes/store/carts/update-payment-session.ts index 279e6512c9..67b9ca8aed 100644 --- a/packages/medusa/src/api/routes/store/carts/update-payment-session.ts +++ b/packages/medusa/src/api/routes/store/carts/update-payment-session.ts @@ -53,5 +53,5 @@ export default async (req, res) => { export class StorePostCartsCartPaymentSessionUpdateReq { @IsObject() - data: object + data: Record } diff --git a/packages/medusa/src/interfaces/index.ts b/packages/medusa/src/interfaces/index.ts index b19351aeef..38c2dfccc7 100644 --- a/packages/medusa/src/interfaces/index.ts +++ b/packages/medusa/src/interfaces/index.ts @@ -9,3 +9,4 @@ export * from "./price-selection-strategy" export * from "./models/base-entity" export * from "./models/soft-deletable-entity" export * from "./search-service" +export * from "./payment-service" diff --git a/packages/medusa/src/interfaces/payment-service.ts b/packages/medusa/src/interfaces/payment-service.ts new file mode 100644 index 0000000000..03a068f675 --- /dev/null +++ b/packages/medusa/src/interfaces/payment-service.ts @@ -0,0 +1,116 @@ +import { TransactionBaseService } from "./transaction-base-service" +import { + Cart, + Customer, + Payment, + PaymentSession, + PaymentSessionStatus, +} from "../models" +import { PaymentService } from "medusa-interfaces" + +export type Data = Record +export type PaymentData = Data +export type PaymentSessionData = Data + +export interface PaymentService> + extends TransactionBaseService { + getIdentifier(): string + + getPaymentData(paymentSession: PaymentSession): Promise + + updatePaymentData( + paymentSessionData: PaymentSessionData, + data: Data + ): Promise + + createPayment(cart: Cart): Promise + + retrievePayment(paymentData: PaymentData): Promise + + updatePayment( + paymentSessionData: PaymentSessionData, + cart: Cart + ): Promise + + authorizePayment( + paymentSession: PaymentSession, + context: Data + ): Promise<{ data: PaymentSessionData; status: PaymentSessionStatus }> + + capturePayment(payment: Payment): Promise + + refundPayment(payment: Payment, refundAmount: number): Promise + + cancelPayment(payment: Payment): Promise + + deletePayment(paymentSession: PaymentSession): Promise + + retrieveSavedMethods(customer: Customer): Promise + + getStatus(data: Data): Promise +} + +export abstract class AbstractPaymentService< + T extends TransactionBaseService + > + extends TransactionBaseService + implements PaymentService +{ + protected constructor(container: unknown, config?: Record) { + super(container, config) + } + + protected static identifier: string + + public getIdentifier(): string { + if (!(this.constructor).identifier) { + throw new Error('Missing static property "identifier".') + } + return (this.constructor).identifier + } + + public abstract getPaymentData( + paymentSession: PaymentSession + ): Promise + + public abstract updatePaymentData( + paymentSessionData: PaymentSessionData, + data: Data + ): Promise + + public abstract createPayment(cart: Cart): Promise + + public abstract retrievePayment(paymentData: PaymentData): Promise + + public abstract updatePayment( + paymentSessionData: PaymentSessionData, + cart: Cart + ): Promise + + public abstract authorizePayment( + paymentSession: PaymentSession, + context: Data + ): Promise<{ data: PaymentSessionData; status: PaymentSessionStatus }> + + public abstract capturePayment(payment: Payment): Promise + + public abstract refundPayment( + payment: Payment, + refundAmount: number + ): Promise + + public abstract cancelPayment(payment: Payment): Promise + + public abstract deletePayment(paymentSession: PaymentSession): Promise + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public retrieveSavedMethods(customer: Customer): Promise { + return Promise.resolve([]) + } + + public abstract getStatus(data: Data): Promise +} + +export function isPaymentService(obj: unknown): boolean { + return obj instanceof AbstractPaymentService || obj instanceof PaymentService +} diff --git a/packages/medusa/src/loaders/defaults.ts b/packages/medusa/src/loaders/defaults.ts index 6d4d26ea43..25324790f6 100644 --- a/packages/medusa/src/loaders/defaults.ts +++ b/packages/medusa/src/loaders/defaults.ts @@ -1,4 +1,8 @@ -import { BasePaymentService, BaseNotificationService, BaseFulfillmentService } from 'medusa-interfaces' +import { + BaseNotificationService, + BaseFulfillmentService, + BasePaymentService, +} from "medusa-interfaces" import { currencies } from "../utils/currencies" import { countries } from "../utils/countries" import { AwilixContainer } from "awilix" @@ -15,11 +19,15 @@ import { TaxProviderService, } from "../services" import { CurrencyRepository } from "../repositories/currency" -import { AbstractTaxService } from "../interfaces" import { FlagRouter } from "../utils/flag-router"; import SalesChannelFeatureFlag from "./feature-flags/sales-channels"; +import { AbstractPaymentService, AbstractTaxService } from "../interfaces" -const silentResolution = (container: AwilixContainer, name: string, logger: Logger): T | never | undefined => { +const silentResolution = ( + container: AwilixContainer, + name: string, + logger: Logger +): T | never | undefined => { try { return container.resolve(name) } catch (err) { @@ -44,15 +52,23 @@ const silentResolution = (container: AwilixContainer, name: string, logger: L `You don't have any ${identifier} provider plugins installed. You may want to add one to your project.` ) } - return; + return } } -export default async ({ container }: { container: AwilixContainer }): Promise => { +export default async ({ + container, +}: { + container: AwilixContainer +}): Promise => { const storeService = container.resolve("storeService") - const currencyRepository = container.resolve("currencyRepository") - const countryRepository = container.resolve("countryRepository") - const profileService = container.resolve("shippingProfileService") + const currencyRepository = + container.resolve("currencyRepository") + const countryRepository = + container.resolve("countryRepository") + const profileService = container.resolve( + "shippingProfileService" + ) const salesChannelService = container.resolve("salesChannelService") const logger = container.resolve("logger") const featureFlagRouter = container.resolve("featureFlagRouter") @@ -104,32 +120,54 @@ export default async ({ container }: { container: AwilixContainer }): Promise(container, "paymentProviders", logger) || [] + silentResolution<(typeof BasePaymentService | AbstractPaymentService)[]>( + container, + "paymentProviders", + logger + ) || [] const payIds = payProviders.map((p) => p.getIdentifier()) - const pProviderService = container.resolve("paymentProviderService") + const pProviderService = container.resolve( + "paymentProviderService" + ) await pProviderService.registerInstalledProviders(payIds) const notiProviders = - silentResolution(container, "notificationProviders", logger) || [] + silentResolution( + container, + "notificationProviders", + logger + ) || [] const notiIds = notiProviders.map((p) => p.getIdentifier()) - const nProviderService = container.resolve("notificationService") + const nProviderService = container.resolve( + "notificationService" + ) await nProviderService.registerInstalledProviders(notiIds) - const fulfilProviders = - silentResolution(container, "fulfillmentProviders", logger) || [] + silentResolution( + container, + "fulfillmentProviders", + logger + ) || [] const fulfilIds = fulfilProviders.map((p) => p.getIdentifier()) - const fProviderService = container.resolve("fulfillmentProviderService") + const fProviderService = container.resolve( + "fulfillmentProviderService" + ) await fProviderService.registerInstalledProviders(fulfilIds) const taxProviders = - silentResolution(container, "taxProviders", logger) || [] + silentResolution( + container, + "taxProviders", + logger + ) || [] const taxIds = taxProviders.map((p) => p.getIdentifier()) - const tProviderService = container.resolve("taxProviderService") + const tProviderService = + container.resolve("taxProviderService") await tProviderService.registerInstalledProviders(taxIds) await profileService.withTransaction(manager).createDefault() diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 98ecaf8ad4..aa16d7c041 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -10,7 +10,6 @@ import { FileService, FulfillmentService, OauthService, - PaymentService, } from "medusa-interfaces" import path from "path" import { EntitySchema } from "typeorm" @@ -20,6 +19,7 @@ import { isCartCompletionStrategy, isFileService, isNotificationService, + isPaymentService, isPriceSelectionStrategy, isSearchService, isTaxCalculationStrategy, @@ -366,7 +366,7 @@ export async function registerServices( throw new Error(message) } - if (loaded.prototype instanceof PaymentService) { + if (isPaymentService(loaded.prototype)) { // Register our payment providers to paymentProviders container.registerAdd( "paymentProviders", diff --git a/packages/medusa/src/migrations/1660040729000-payment_session_uniq_cartId_providerId.ts b/packages/medusa/src/migrations/1660040729000-payment_session_uniq_cartId_providerId.ts new file mode 100644 index 0000000000..d738af3d86 --- /dev/null +++ b/packages/medusa/src/migrations/1660040729000-payment_session_uniq_cartId_providerId.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class paymentSessionUniqCartIdProviderId1660040729000 implements MigrationInterface { + name = "paymentSessionUniqCartIdProviderId1660040729000" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE UNIQUE INDEX "UniqPaymentSessionCartIdProviderId" ON "payment_session" ("cart_id", "provider_id")`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "UniqPaymentSessionCartIdProviderId"`) + } +} diff --git a/packages/medusa/src/models/payment-session.ts b/packages/medusa/src/models/payment-session.ts index b458b26892..2945f11041 100644 --- a/packages/medusa/src/models/payment-session.ts +++ b/packages/medusa/src/models/payment-session.ts @@ -22,6 +22,7 @@ export enum PaymentSessionStatus { } @Unique("OneSelected", ["cart_id", "is_selected"]) +@Unique("UniqPaymentSessionCartIdProviderId", ["cart_id", "provider_id"]) @Entity() export class PaymentSession extends BaseEntity { @Index() diff --git a/packages/medusa/src/models/payment.ts b/packages/medusa/src/models/payment.ts index 607e9a0a40..1fe31488db 100644 --- a/packages/medusa/src/models/payment.ts +++ b/packages/medusa/src/models/payment.ts @@ -63,10 +63,10 @@ export class Payment extends BaseEntity { data: Record @Column({ type: resolveDbType("timestamptz"), nullable: true }) - captured_at: Date + captured_at: Date | string @Column({ type: resolveDbType("timestamptz"), nullable: true }) - canceled_at: Date + canceled_at: Date | string @DbAwareColumn({ type: "jsonb", nullable: true }) metadata: Record diff --git a/packages/medusa/src/services/__mocks__/test-pay.js b/packages/medusa/src/services/__mocks__/test-pay.js new file mode 100644 index 0000000000..34017ceca0 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/test-pay.js @@ -0,0 +1,43 @@ +export const testPayServiceMock = { + identifier: "test-pay", + getIdentifier: "test-pay", + withTransaction: function () { + return this + }, + getStatus: jest.fn().mockResolvedValue(Promise.resolve("authorised")), + retrieveSavedMethods: jest.fn().mockResolvedValue(Promise.resolve([])), + getPaymentData: jest.fn().mockResolvedValue(Promise.resolve({})), + createPayment: jest.fn().mockImplementation(() => { + return {} + }), + retrievePayment: jest.fn().mockImplementation(() => { + return {} + }), + updatePayment: jest.fn().mockImplementation(() => { + return {} + }), + deletePayment: jest.fn().mockImplementation(() => { + return {} + }), + authorizePayment: jest.fn().mockImplementation(() => { + return {} + }), + updatePaymentData: jest.fn().mockImplementation(() => { + return {} + }), + cancelPayment: jest.fn().mockImplementation(() => { + return {} + }), + capturePayment: jest.fn().mockImplementation(() => { + return {} + }), + refundPayment: jest.fn().mockImplementation(() => { + return {} + }) +} + +const mock = jest.fn().mockImplementation(() => { + return testPayServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index b354f11c82..f6e9a5f179 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -61,6 +61,8 @@ describe("CartService", () => { undefined, { where: { id: IdMap.getId("emptyCart") }, + select: undefined, + relations: undefined, } ) }) diff --git a/packages/medusa/src/services/__tests__/payment-provider.js b/packages/medusa/src/services/__tests__/payment-provider.js index e071e2fb7f..17eec0ebab 100644 --- a/packages/medusa/src/services/__tests__/payment-provider.js +++ b/packages/medusa/src/services/__tests__/payment-provider.js @@ -1,7 +1,8 @@ import { MockManager, MockRepository } from "medusa-test-utils" import PaymentProviderService from "../payment-provider" +import { testPayServiceMock } from "../__mocks__/test-pay" -describe("ProductService", () => { +describe("PaymentProviderService", () => { describe("retrieveProvider", () => { const container = { manager: MockManager, @@ -33,6 +34,9 @@ describe("ProductService", () => { manager: MockManager, paymentSessionRepository: MockRepository(), pp_default_provider: { + withTransaction: function () { + return this + }, createPayment, }, } @@ -67,6 +71,9 @@ describe("ProductService", () => { }), }), pp_default_provider: { + withTransaction: function () { + return this + }, updatePayment, }, } @@ -97,3 +104,183 @@ describe("ProductService", () => { }) }) }) + +describe(`PaymentProviderService`, () => { + const container = { + manager: MockManager, + paymentSessionRepository: MockRepository({ + findOne: () => + Promise.resolve({ + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + }), + }), + paymentRepository: MockRepository({ + findOne: () => + Promise.resolve({ + id: "pay_jadazdjk", + provider_id: "default_provider", + data: { + id: "1234", + }, + }), + find: () => + Promise.resolve([{ + id: "pay_jadazdjk", + provider_id: "default_provider", + data: { + id: "1234", + }, + captured_at: new Date(), + amount: 100, + amount_refunded: 0 + }]), + }), + refundRepository: MockRepository(), + pp_default_provider: testPayServiceMock, + } + const providerService = new PaymentProviderService(container) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("successfully retrieves payment provider", () => { + const provider = providerService.retrieveProvider("default_provider") + expect(provider.identifier).toEqual("test-pay") + }) + + it("successfully creates session", async () => { + await providerService.createSession("default_provider", { + total: 100, + }) + + expect(testPayServiceMock.createPayment).toBeCalledTimes(1) + expect(testPayServiceMock.createPayment).toBeCalledWith({ + total: 100, + }) + }) + + it("successfully update session", async () => { + await providerService.updateSession( + { + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + }, + { + total: 100, + } + ) + + expect(testPayServiceMock.updatePayment).toBeCalledTimes(1) + expect(testPayServiceMock.updatePayment).toBeCalledWith( + { id: "1234" }, + { + total: 100, + } + ) + }) + + it("successfully refresh session", async () => { + await providerService.refreshSession( + { + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + }, + { + total: 100, + } + ) + + expect(testPayServiceMock.deletePayment).toBeCalledTimes(1) + expect(testPayServiceMock.createPayment).toBeCalledTimes(1) + }) + + it("successfully delete session", async () => { + await providerService.deleteSession( + { + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + } + ) + + expect(testPayServiceMock.deletePayment).toBeCalledTimes(1) + }) + + it("successfully delete session", async () => { + await providerService.deleteSession( + { + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + } + ) + + expect(testPayServiceMock.deletePayment).toBeCalledTimes(1) + }) + + it("successfully authorize payment", async () => { + await providerService.authorizePayment( + { + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + }, + {} + ) + + expect(testPayServiceMock.authorizePayment).toBeCalledTimes(1) + }) + + it("successfully update session data", async () => { + await providerService.updateSessionData( + { + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + }, + {} + ) + + expect(testPayServiceMock.updatePaymentData).toBeCalledTimes(1) + }) + + it("successfully cancel payment", async () => { + await providerService.cancelPayment({ + id: "pay_jadazdjk" + }) + expect(testPayServiceMock.cancelPayment).toBeCalledTimes(1) + }) + + it("successfully capture payment", async () => { + await providerService.capturePayment({ + id: "pay_jadazdjk" + }) + expect(testPayServiceMock.capturePayment).toBeCalledTimes(1) + }) + + it("successfully refund payment", async () => { + await providerService.refundPayment([{ + id: "pay_jadazdjk" + }], 50) + expect(testPayServiceMock.refundPayment).toBeCalledTimes(1) + }) +}) diff --git a/packages/medusa/src/services/__tests__/swap.js b/packages/medusa/src/services/__tests__/swap.js index aaf1a4330e..ebf3df9733 100644 --- a/packages/medusa/src/services/__tests__/swap.js +++ b/packages/medusa/src/services/__tests__/swap.js @@ -1,4 +1,3 @@ -import paymentService from "medusa-interfaces/dist/payment-service" import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import SwapService from "../swap" import { InventoryServiceMock } from "../__mocks__/inventory" diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index bc8ee5d24d..8caa2acdb4 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -4,7 +4,6 @@ import { DeepPartial, EntityManager, In } from "typeorm" import { TransactionBaseService } from "../interfaces" import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy" import { - DiscountRuleType, Address, Cart, CustomShippingOption, @@ -13,6 +12,8 @@ import { LineItem, ShippingMethod, SalesChannel, + DiscountRuleType, + PaymentSession, } from "../models" import { AddressRepository } from "../repositories/address" import { CartRepository } from "../repositories/cart" @@ -1310,7 +1311,10 @@ class CartService extends TransactionBaseService { * @param update - the data to update the payment session with * @return the resulting cart */ - async updatePaymentSession(cartId: string, update: object): Promise { + async updatePaymentSession( + cartId: string, + update: Record + ): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { @@ -1385,14 +1389,14 @@ class CartService extends TransactionBaseService { ) } - const session = await this.paymentProviderService_ + const session = (await this.paymentProviderService_ .withTransaction(transactionManager) - .authorizePayment(cart.payment_session, context) + .authorizePayment(cart.payment_session, context)) as PaymentSession - const freshCart = await this.retrieve(cart.id, { + const freshCart = (await this.retrieve(cart.id, { select: ["total"], relations: ["payment_sessions", "items", "items.adjustments"], - }) + })) as Cart & { payment_session: PaymentSession } if (session.status === "authorized") { freshCart.payment = await this.paymentProviderService_ diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index c68bb4a4f9..f99105e0bc 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -30,6 +30,7 @@ import { Order, OrderStatus, Payment, + PaymentSession, PaymentStatus, Return, Swap, @@ -531,7 +532,6 @@ class OrderService extends TransactionBaseService { // Would be the case if a discount code is applied that covers the item // total if (total !== 0) { - // Throw if payment method does not exist if (!payment) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -543,7 +543,6 @@ class OrderService extends TransactionBaseService { .withTransaction(manager) .getStatus(payment) - // If payment status is not authorized, we throw if (paymentStatus !== "authorized") { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, diff --git a/packages/medusa/src/services/payment-provider.js b/packages/medusa/src/services/payment-provider.js deleted file mode 100644 index 613811d59c..0000000000 --- a/packages/medusa/src/services/payment-provider.js +++ /dev/null @@ -1,438 +0,0 @@ -import { BaseService } from "medusa-interfaces" -import { MedusaError } from "medusa-core-utils" - -/** - * Helps retrive payment providers - */ -class PaymentProviderService extends BaseService { - constructor(container) { - super() - - /** @private {logger} */ - this.container_ = container - - this.manager_ = container.manager - - this.paymentSessionRepository_ = container.paymentSessionRepository - - this.paymentRepository_ = container.paymentRepository - - this.refundRepository_ = container.refundRepository - } - - withTransaction(manager) { - if (!manager) { - return this - } - - const cloned = new PaymentProviderService(this.container_) - cloned.transactionManager_ = manager - cloned.manager_ = manager - - return cloned - } - - async registerInstalledProviders(providers) { - const { manager, paymentProviderRepository } = this.container_ - - const model = manager.getCustomRepository(paymentProviderRepository) - await model.update({}, { is_installed: false }) - - for (const p of providers) { - const n = model.create({ id: p, is_installed: true }) - await model.save(n) - } - } - - async list() { - const { manager, paymentProviderRepository } = this.container_ - const ppRepo = manager.getCustomRepository(paymentProviderRepository) - - return await ppRepo.find({}) - } - - async retrievePayment(id, relations = []) { - const paymentRepo = this.manager_.getCustomRepository( - this.paymentRepository_ - ) - const validatedId = this.validateId_(id) - - const query = { - where: { id: validatedId }, - } - - if (relations.length) { - query.relations = relations - } - - const payment = await paymentRepo.findOne(query) - - if (!payment) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Payment with ${id} was not found` - ) - } - - return payment - } - - listPayments( - selector, - config = { skip: 0, take: 50, order: { created_at: "DESC" } } - ) { - const payRepo = this.manager_.getCustomRepository(this.paymentRepository_) - const query = this.buildQuery_(selector, config) - return payRepo.find(query) - } - - async retrieveSession(id, relations = []) { - const sessionRepo = this.manager_.getCustomRepository( - this.paymentSessionRepository_ - ) - const validatedId = this.validateId_(id) - - const query = { - where: { id: validatedId }, - } - - if (relations.length) { - query.relations = relations - } - - const session = await sessionRepo.findOne(query) - - if (!session) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Payment Session with ${id} was not found` - ) - } - - return session - } - - /** - * Creates a payment session with the given provider. - * @param {string} providerId - the id of the provider to create payment with - * @param {Cart} cart - a cart object used to calculate the amount, etc. from - * @return {Promise} the payment session - */ - async createSession(providerId, cart) { - return this.atomicPhase_(async (manager) => { - const provider = this.retrieveProvider(providerId) - const sessionData = await provider.createPayment(cart) - - const sessionRepo = manager.getCustomRepository( - this.paymentSessionRepository_ - ) - - const toCreate = { - cart_id: cart.id, - provider_id: providerId, - data: sessionData, - status: "pending", - } - - const created = sessionRepo.create(toCreate) - const result = await sessionRepo.save(created) - - return result - }) - } - - /** - * Refreshes a payment session with the given provider. - * This means, that we delete the current one and create a new. - * @param {PaymentSession} paymentSession - the payment session object to - * update - * @param {Cart} cart - a cart object used to calculate the amount, etc. from - * @return {Promise} the payment session - */ - async refreshSession(paymentSession, cart) { - return this.atomicPhase_(async (manager) => { - const session = await this.retrieveSession(paymentSession.id) - - const provider = this.retrieveProvider(paymentSession.provider_id) - await provider.deletePayment(session) - - const sessionRepo = manager.getCustomRepository( - this.paymentSessionRepository_ - ) - - await sessionRepo.remove(session) - - const sessionData = await provider.createPayment(cart) - const toCreate = { - cart_id: cart.id, - provider_id: session.provider_id, - data: sessionData, - is_selected: true, - status: "pending", - } - - const created = sessionRepo.create(toCreate) - const result = await sessionRepo.save(created) - - return result - }) - } - - /** - * Updates an existing payment session. - * @param {PaymentSession} paymentSession - the payment session object to - * update - * @param {Cart} cart - the cart object to update for - * @return {Promise} the updated payment session - */ - updateSession(paymentSession, cart) { - return this.atomicPhase_(async (manager) => { - const session = await this.retrieveSession(paymentSession.id) - - const provider = this.retrieveProvider(paymentSession.provider_id) - session.data = await provider.updatePayment(paymentSession.data, cart) - - const sessionRepo = manager.getCustomRepository( - this.paymentSessionRepository_ - ) - return sessionRepo.save(session) - }) - } - - deleteSession(paymentSession) { - return this.atomicPhase_(async (manager) => { - const session = await this.retrieveSession(paymentSession.id).catch( - (_) => undefined - ) - - if (!session) { - return Promise.resolve() - } - - const provider = this.retrieveProvider(paymentSession.provider_id) - await provider.deletePayment(paymentSession) - - const sessionRepo = manager.getCustomRepository( - this.paymentSessionRepository_ - ) - - return sessionRepo.remove(session) - }) - } - - /** - * Finds a provider given an id - * @param {string} providerId - the id of the provider to get - * @return {PaymentService} the payment provider - */ - retrieveProvider(providerId) { - try { - let provider - if (providerId === "system") { - provider = this.container_[`systemPaymentProviderService`] - } else { - provider = this.container_[`pp_${providerId}`] - } - - return provider - } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a payment provider with id: ${providerId}` - ) - } - } - - async createPayment(cart) { - return this.atomicPhase_(async (manager) => { - const { payment_session: paymentSession, region, total } = cart - const provider = this.retrieveProvider(paymentSession.provider_id) - const paymentData = await provider.getPaymentData(paymentSession) - - const paymentRepo = manager.getCustomRepository(this.paymentRepository_) - - const created = paymentRepo.create({ - provider_id: paymentSession.provider_id, - amount: total, - currency_code: region.currency_code, - data: paymentData, - cart_id: cart.id, - }) - - return paymentRepo.save(created) - }) - } - - async updatePayment(paymentId, update) { - return this.atomicPhase_(async (manager) => { - const payment = await this.retrievePayment(paymentId) - - if ("order_id" in update) { - payment.order_id = update.order_id - } - - if ("swap_id" in update) { - payment.swap_id = update.swap_id - } - - const payRepo = manager.getCustomRepository(this.paymentRepository_) - return payRepo.save(payment) - }) - } - - async authorizePayment(paymentSession, context) { - return this.atomicPhase_(async (manager) => { - const session = await this.retrieveSession(paymentSession.id).catch( - (_) => undefined - ) - - if (!session) { - return Promise.resolve() - } - - const provider = this.retrieveProvider(paymentSession.provider_id) - const { status, data } = await provider - .withTransaction(manager) - .authorizePayment(session, context) - - session.data = data - session.status = status - - const sessionRepo = manager.getCustomRepository( - this.paymentSessionRepository_ - ) - return sessionRepo.save(session) - }) - } - - async updateSessionData(paySession, update) { - return this.atomicPhase_(async (manager) => { - const session = await this.retrieveSession(paySession.id) - - const provider = this.retrieveProvider(paySession.provider_id) - - session.data = await provider.updatePaymentData(paySession.data, update) - session.status = paySession.status - - const sessionRepo = manager.getCustomRepository( - this.paymentSessionRepository_ - ) - return sessionRepo.save(session) - }) - } - - async cancelPayment(paymentObj) { - return this.atomicPhase_(async (manager) => { - const payment = await this.retrievePayment(paymentObj.id) - const provider = this.retrieveProvider(payment.provider_id) - payment.data = await provider.cancelPayment(payment) - - const now = new Date() - payment.canceled_at = now.toISOString() - - const paymentRepo = manager.getCustomRepository(this.paymentRepository_) - return await paymentRepo.save(payment) - }) - } - - async getStatus(payment) { - const provider = this.retrieveProvider(payment.provider_id) - return provider.getStatus(payment.data) - } - - async capturePayment(paymentObj) { - return this.atomicPhase_(async (manager) => { - const payment = await this.retrievePayment(paymentObj.id) - - const provider = this.retrieveProvider(payment.provider_id) - payment.data = await provider.capturePayment(payment) - - const now = new Date() - payment.captured_at = now.toISOString() - - const paymentRepo = manager.getCustomRepository(this.paymentRepository_) - return paymentRepo.save(payment) - }) - } - - async refundPayment(payObjs, amount, reason, note) { - return this.atomicPhase_(async (manager) => { - const payments = await this.listPayments({ id: payObjs.map((p) => p.id) }) - - let order_id - const refundable = payments.reduce((acc, next) => { - order_id = next.order_id - if (next.captured_at) { - return (acc += next.amount - next.amount_refunded) - } - - return acc - }, 0) - - if (refundable < amount) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Refund amount is too high" - ) - } - - let balance = amount - - const used = [] - - const paymentRepo = manager.getCustomRepository(this.paymentRepository_) - let toRefund = payments.find((p) => p.amount - p.amount_refunded > 0) - while (toRefund) { - const currentRefundable = toRefund.amount - toRefund.amount_refunded - - const refundAmount = Math.min(currentRefundable, balance) - - const provider = this.retrieveProvider(toRefund.provider_id) - toRefund.data = await provider.refundPayment(toRefund, refundAmount) - toRefund.amount_refunded += refundAmount - await paymentRepo.save(toRefund) - - balance -= refundAmount - - used.push(toRefund.id) - - if (balance > 0) { - toRefund = payments.find( - (p) => p.amount - p.amount_refunded > 0 && !used.includes(p.id) - ) - } else { - toRefund = null - } - } - - const refundRepo = manager.getCustomRepository(this.refundRepository_) - - const toCreate = { - order_id, - amount, - reason, - note, - } - - const created = refundRepo.create(toCreate) - return refundRepo.save(created) - }) - } - - async retrieveRefund(id, config = {}) { - const refRepo = this.manager_.getCustomRepository(this.refundRepository_) - const query = this.buildQuery_({ id }, config) - const refund = await refRepo.findOne(query) - - if (!refund) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `A refund with ${id} was not found` - ) - } - - return refund - } -} - -export default PaymentProviderService diff --git a/packages/medusa/src/services/payment-provider.ts b/packages/medusa/src/services/payment-provider.ts new file mode 100644 index 0000000000..b3f5de7cb3 --- /dev/null +++ b/packages/medusa/src/services/payment-provider.ts @@ -0,0 +1,544 @@ +import { MedusaError } from "medusa-core-utils" +import { BasePaymentService } from "medusa-interfaces" +import { AbstractPaymentService, TransactionBaseService } from "../interfaces" +import { EntityManager } from "typeorm" +import { PaymentSessionRepository } from "../repositories/payment-session" +import { PaymentRepository } from "../repositories/payment" +import { RefundRepository } from "../repositories/refund" +import { PaymentProviderRepository } from "../repositories/payment-provider" +import { buildQuery } from "../utils" +import { FindConfig, Selector } from "../types/common" +import { + Cart, + Payment, + PaymentProvider, + PaymentSession, + PaymentSessionStatus, + Refund, +} from "../models" + +type PaymentProviderKey = `pp_${string}` | "systemPaymentProviderService" +type InjectedDependencies = { + manager: EntityManager + paymentSessionRepository: typeof PaymentSessionRepository + paymentProviderRepository: typeof PaymentProviderRepository + paymentRepository: typeof PaymentRepository + refundRepository: typeof RefundRepository +} & { + [key in `${PaymentProviderKey}`]: + | AbstractPaymentService + | typeof BasePaymentService +} + +/** + * Helps retrieve payment providers + */ +export default class PaymentProviderService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + protected readonly container_: InjectedDependencies + protected readonly paymentSessionRepository_: typeof PaymentSessionRepository + protected readonly paymentProviderRepository_: typeof PaymentProviderRepository + protected readonly paymentRepository_: typeof PaymentRepository + protected readonly refundRepository_: typeof RefundRepository + + constructor(container: InjectedDependencies) { + super(container) + + this.container_ = container + this.manager_ = container.manager + this.paymentSessionRepository_ = container.paymentSessionRepository + this.paymentProviderRepository_ = container.paymentProviderRepository + this.paymentRepository_ = container.paymentRepository + this.refundRepository_ = container.refundRepository + } + + async registerInstalledProviders(providerIds: string[]): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const model = transactionManager.getCustomRepository( + this.paymentProviderRepository_ + ) + await model.update({}, { is_installed: false }) + + await Promise.all( + providerIds.map(async (providerId) => { + const provider = model.create({ + id: providerId, + is_installed: true, + }) + return await model.save(provider) + }) + ) + }) + } + + async list(): Promise { + const ppRepo = this.manager_.getCustomRepository( + this.paymentProviderRepository_ + ) + return await ppRepo.find() + } + + async retrievePayment( + id: string, + relations: string[] = [] + ): Promise { + const paymentRepo = this.manager_.getCustomRepository( + this.paymentRepository_ + ) + const query = { + where: { id }, + relations: [] as string[], + } + + if (relations.length) { + query.relations = relations + } + + const payment = await paymentRepo.findOne(query) + + if (!payment) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Payment with ${id} was not found` + ) + } + + return payment + } + + async listPayments( + selector: Selector, + config: FindConfig = { + skip: 0, + take: 50, + order: { created_at: "DESC" }, + } + ): Promise { + const payRepo = this.manager_.getCustomRepository(this.paymentRepository_) + const query = buildQuery(selector, config) + return await payRepo.find(query) + } + + async retrieveSession( + id: string, + relations: string[] = [] + ): Promise { + const sessionRepo = this.manager_.getCustomRepository( + this.paymentSessionRepository_ + ) + + const query = { + where: { id }, + relations: [] as string[], + } + + if (relations.length) { + query.relations = relations + } + + const session = await sessionRepo.findOne(query) + + if (!session) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Payment Session with ${id} was not found` + ) + } + + return session + } + + /** + * Creates a payment session with the given provider. + * @param providerId - the id of the provider to create payment with + * @param cart - a cart object used to calculate the amount, etc. from + * @return the payment session + */ + async createSession(providerId: string, cart: Cart): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const provider = this.retrieveProvider(providerId) + const sessionData = await provider + .withTransaction(transactionManager) + .createPayment(cart) + + const sessionRepo = transactionManager.getCustomRepository( + this.paymentSessionRepository_ + ) + + const toCreate = { + cart_id: cart.id, + provider_id: providerId, + data: sessionData, + status: "pending", + } + + const created = sessionRepo.create(toCreate) + return await sessionRepo.save(created) + }) + } + + /** + * Refreshes a payment session with the given provider. + * This means, that we delete the current one and create a new. + * @param paymentSession - the payment session object to + * update + * @param cart - a cart object used to calculate the amount, etc. from + * @return the payment session + */ + async refreshSession( + paymentSession: PaymentSession, + cart: Cart + ): Promise { + return this.atomicPhase_(async (transactionManager) => { + const session = await this.retrieveSession(paymentSession.id) + const provider = this.retrieveProvider(paymentSession.provider_id) + await provider.withTransaction(transactionManager).deletePayment(session) + + const sessionRepo = transactionManager.getCustomRepository( + this.paymentSessionRepository_ + ) + + await sessionRepo.remove(session) + + const sessionData = await provider + .withTransaction(transactionManager) + .createPayment(cart) + + const toCreate = { + cart_id: cart.id, + provider_id: session.provider_id, + data: sessionData, + is_selected: true, + status: "pending", + } + + const created = sessionRepo.create(toCreate) + return await sessionRepo.save(created) + }) + } + + /** + * Updates an existing payment session. + * @param paymentSession - the payment session object to + * update + * @param cart - the cart object to update for + * @return the updated payment session + */ + async updateSession( + paymentSession: PaymentSession, + cart: Cart + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const session = await this.retrieveSession(paymentSession.id) + const provider = this.retrieveProvider(paymentSession.provider_id) + session.data = await provider + .withTransaction(transactionManager) + .updatePayment(paymentSession.data, cart) + + const sessionRepo = transactionManager.getCustomRepository( + this.paymentSessionRepository_ + ) + return sessionRepo.save(session) + }) + } + + async deleteSession( + paymentSession: PaymentSession + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const session = await this.retrieveSession(paymentSession.id).catch( + () => void 0 + ) + + if (!session) { + return + } + + const provider = this.retrieveProvider(paymentSession.provider_id) + await provider + .withTransaction(transactionManager) + .deletePayment(paymentSession) + + const sessionRepo = transactionManager.getCustomRepository( + this.paymentSessionRepository_ + ) + + return sessionRepo.remove(session) + }) + } + + /** + * Finds a provider given an id + * @param {string} providerId - the id of the provider to get + * @return {PaymentService} the payment provider + */ + retrieveProvider< + TProvider extends AbstractPaymentService | typeof BasePaymentService + >( + providerId: string + ): TProvider extends AbstractPaymentService + ? AbstractPaymentService + : typeof BasePaymentService { + try { + let provider + if (providerId === "system") { + provider = this.container_[`systemPaymentProviderService`] + } else { + provider = this.container_[`pp_${providerId}`] + } + + return provider + } catch (err) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Could not find a payment provider with id: ${providerId}` + ) + } + } + + async createPayment( + cart: Cart & { payment_session: PaymentSession } + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const { payment_session: paymentSession, region, total } = cart + + const provider = this.retrieveProvider(paymentSession.provider_id) + const paymentData = await provider + .withTransaction(transactionManager) + .getPaymentData(paymentSession) + + const paymentRepo = transactionManager.getCustomRepository( + this.paymentRepository_ + ) + + const created = paymentRepo.create({ + provider_id: paymentSession.provider_id, + amount: total, + currency_code: region.currency_code, + data: paymentData, + cart_id: cart.id, + }) + + return paymentRepo.save(created) + }) + } + + async updatePayment( + paymentId: string, + data: { order_id?: string; swap_id?: string } + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const payment = await this.retrievePayment(paymentId) + + if (data?.order_id) { + payment.order_id = data.order_id + } + + if (data?.swap_id) { + payment.swap_id = data.swap_id + } + + const payRepo = transactionManager.getCustomRepository( + this.paymentRepository_ + ) + return payRepo.save(payment) + }) + } + + async authorizePayment( + paymentSession: PaymentSession, + context: Record + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const session = await this.retrieveSession(paymentSession.id).catch( + () => void 0 + ) + + if (!session) { + return + } + + const provider = this.retrieveProvider(paymentSession.provider_id) + const { status, data } = await provider + .withTransaction(transactionManager) + .authorizePayment(session, context) + + session.data = data + session.status = status + + const sessionRepo = transactionManager.getCustomRepository( + this.paymentSessionRepository_ + ) + return sessionRepo.save(session) + }) + } + + async updateSessionData( + paymentSession: PaymentSession, + data: Record + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const session = await this.retrieveSession(paymentSession.id) + + const provider = this.retrieveProvider(paymentSession.provider_id) + + session.data = await provider + .withTransaction(transactionManager) + .updatePaymentData(paymentSession.data, data) + session.status = paymentSession.status + + const sessionRepo = transactionManager.getCustomRepository( + this.paymentSessionRepository_ + ) + return sessionRepo.save(session) + }) + } + + async cancelPayment( + paymentObj: Partial & { id: string } + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const payment = await this.retrievePayment(paymentObj.id) + const provider = this.retrieveProvider(payment.provider_id) + payment.data = await provider + .withTransaction(transactionManager) + .cancelPayment(payment) + + const now = new Date() + payment.canceled_at = now.toISOString() + + const paymentRepo = transactionManager.getCustomRepository( + this.paymentRepository_ + ) + return await paymentRepo.save(payment) + }) + } + + async getStatus(payment: Payment): Promise { + const provider = this.retrieveProvider(payment.provider_id) + return await provider.withTransaction(this.manager_).getStatus(payment.data) + } + + async capturePayment( + paymentObj: Partial & { id: string } + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const payment = await this.retrievePayment(paymentObj.id) + const provider = this.retrieveProvider(payment.provider_id) + payment.data = await provider + .withTransaction(transactionManager) + .capturePayment(payment) + + const now = new Date() + payment.captured_at = now.toISOString() + + const paymentRepo = transactionManager.getCustomRepository( + this.paymentRepository_ + ) + return paymentRepo.save(payment) + }) + } + + async refundPayment( + payObjs: Payment[], + amount: number, + reason: string, + note?: string + ): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const payments = await this.listPayments({ + id: payObjs.map((p) => p.id), + }) + + let order_id!: string + const refundable = payments.reduce((acc, next) => { + order_id = next.order_id + if (next.captured_at) { + return (acc += next.amount - next.amount_refunded) + } + + return acc + }, 0) + + if (refundable < amount) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Refund amount is higher that the refundable amount" + ) + } + + let balance = amount + + const used: string[] = [] + + const paymentRepo = transactionManager.getCustomRepository( + this.paymentRepository_ + ) + let paymentToRefund = payments.find( + (payment) => payment.amount - payment.amount_refunded > 0 + ) + + while (paymentToRefund) { + const currentRefundable = + paymentToRefund.amount - paymentToRefund.amount_refunded + + const refundAmount = Math.min(currentRefundable, balance) + + const provider = this.retrieveProvider(paymentToRefund.provider_id) + paymentToRefund.data = await provider + .withTransaction(transactionManager) + .refundPayment(paymentToRefund, refundAmount) + + paymentToRefund.amount_refunded += refundAmount + await paymentRepo.save(paymentToRefund) + + balance -= refundAmount + + used.push(paymentToRefund.id) + + if (balance > 0) { + paymentToRefund = payments.find( + (payment) => + payment.amount - payment.amount_refunded > 0 && + !used.includes(payment.id) + ) + } else { + paymentToRefund = undefined + } + } + + const refundRepo = transactionManager.getCustomRepository( + this.refundRepository_ + ) + + const toCreate = { + order_id, + amount, + reason, + note, + } + + const created = refundRepo.create(toCreate) + return refundRepo.save(created) + }) + } + + async retrieveRefund( + id: string, + config: FindConfig = {} + ): Promise { + const refRepo = this.manager_.getCustomRepository(this.refundRepository_) + const query = buildQuery({ id }, config) + const refund = await refRepo.findOne(query) + + if (!refund) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `A refund with ${id} was not found` + ) + } + + return refund + } +} diff --git a/packages/medusa/src/services/system-payment-provider.js b/packages/medusa/src/services/system-payment-provider.js index f8254b99dc..737c7490a0 100644 --- a/packages/medusa/src/services/system-payment-provider.js +++ b/packages/medusa/src/services/system-payment-provider.js @@ -1,10 +1,10 @@ -import { BaseService } from "medusa-interfaces" +import { TransactionBaseService } from "../interfaces" -class SystemProviderService extends BaseService { +class SystemProviderService extends TransactionBaseService { static identifier = "system" constructor(_) { - super() + super(_) } async createPayment(_) { diff --git a/packages/medusa/tsconfig.json b/packages/medusa/tsconfig.json index c54526fd0d..0fc6130e78 100644 --- a/packages/medusa/tsconfig.json +++ b/packages/medusa/tsconfig.json @@ -22,14 +22,11 @@ "skipLibCheck": true, "downlevelIteration": true // to use ES5 specific tooling }, - "include": [ - "./src/**/*", - "index.d.ts" - ], + "include": ["./src/**/*", "index.d.ts"], "exclude": [ "./dist/**/*", "./src/**/__tests__", "./src/**/__mocks__", "node_modules" ] -} +} \ No newline at end of file From c31290c911450a06d5e4da3dc5e4e3977071a6ea Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 10 Aug 2022 17:45:48 +0200 Subject: [PATCH 13/34] feat(medusa): Refactor undefined check into a single util (#2024) --- .changeset/tiny-sheep-compare.md | 5 ++++ .../api/routes/admin/batch/list-batch-jobs.ts | 6 ++--- .../routes/admin/discounts/list-discounts.ts | 3 ++- .../admin/gift-cards/list-gift-cards.ts | 3 ++- .../api/routes/admin/orders/request-return.ts | 3 ++- .../price-lists/list-price-list-products.ts | 3 ++- .../admin/product-tags/list-product-tags.ts | 3 ++- .../admin/product-types/list-product-types.ts | 3 ++- .../routes/admin/returns/receive-return.ts | 3 ++- .../routes/admin/tax-rates/create-tax-rate.ts | 7 +++--- .../routes/admin/tax-rates/update-tax-rate.ts | 7 +++--- .../admin/tax-rates/utils/get-query-config.ts | 9 ++++---- .../src/api/routes/store/carts/create-cart.ts | 8 +++---- .../routes/store/products/list-products.ts | 3 ++- .../controllers/customers/list-customers.ts | 3 ++- .../medusa/src/loaders/feature-flags/index.ts | 5 ++-- packages/medusa/src/loaders/services.ts | 3 ++- packages/medusa/src/loaders/strategies.ts | 4 ++-- packages/medusa/src/repositories/tax-rate.ts | 5 ++-- packages/medusa/src/services/cart.ts | 22 +++++++----------- packages/medusa/src/services/claim.ts | 4 ++-- .../medusa/src/services/customer-group.ts | 5 ++-- packages/medusa/src/services/customer.ts | 6 ++--- packages/medusa/src/services/fulfillment.ts | 4 ++-- packages/medusa/src/services/gift-card.ts | 8 +++---- .../medusa/src/services/product-variant.ts | 3 ++- packages/medusa/src/services/product.ts | 10 ++++---- packages/medusa/src/services/return.js | 3 ++- .../medusa/src/services/shipping-option.ts | 23 ++++++++----------- packages/medusa/src/services/swap.js | 5 ++-- packages/medusa/src/services/tax-rate.ts | 3 ++- packages/medusa/src/services/totals.ts | 5 ++-- .../medusa/src/strategies/price-selection.ts | 4 ++-- packages/medusa/src/utils/get-query-config.ts | 11 +++++---- packages/medusa/src/utils/index.ts | 1 + packages/medusa/src/utils/is-defined.ts | 3 +++ .../src/utils/remove-undefined-properties.ts | 6 +++-- 37 files changed, 118 insertions(+), 94 deletions(-) create mode 100644 .changeset/tiny-sheep-compare.md create mode 100644 packages/medusa/src/utils/is-defined.ts diff --git a/.changeset/tiny-sheep-compare.md b/.changeset/tiny-sheep-compare.md new file mode 100644 index 0000000000..0fc8f082ef --- /dev/null +++ b/.changeset/tiny-sheep-compare.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Add new `isDefined` utility diff --git a/packages/medusa/src/api/routes/admin/batch/list-batch-jobs.ts b/packages/medusa/src/api/routes/admin/batch/list-batch-jobs.ts index d8a863dffb..2360fc801d 100644 --- a/packages/medusa/src/api/routes/admin/batch/list-batch-jobs.ts +++ b/packages/medusa/src/api/routes/admin/batch/list-batch-jobs.ts @@ -6,6 +6,7 @@ import { DateComparisonOperator } from "../../../../types/common" import { IsType } from "../../../../utils/validators/is-type" import { Request } from "express" import { pickBy } from "lodash" +import { isDefined } from "../../../../utils" /** * @oas [get] /batch-jobs @@ -238,9 +239,8 @@ export default async (req: Request, res) => { const created_by = req.user?.id || req.user?.userId const [jobs, count] = await batchService.listAndCount( - pickBy( - { created_by, ...(req.filterableFields ?? {}) }, - (val) => typeof val !== "undefined" + pickBy({ created_by, ...(req.filterableFields ?? {}) }, (val) => + isDefined(val) ), req.listConfig ) diff --git a/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts b/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts index 9fc7a351ce..7a725db09a 100644 --- a/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts +++ b/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts @@ -14,6 +14,7 @@ import { Discount } from "../../../.." import DiscountService from "../../../../services/discount" import { FindConfig } from "../../../../types/common" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [get] /discounts @@ -84,7 +85,7 @@ export default async (req, res) => { const filterableFields = _.omit(validated, ["limit", "offset", "expand"]) const [discounts, count] = await discountService.listAndCount( - pickBy(filterableFields, (val) => typeof val !== "undefined"), + pickBy(filterableFields, (val) => isDefined(val)), listConfig ) diff --git a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts index 897732cd82..fb1e373337 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts +++ b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts @@ -4,6 +4,7 @@ import { GiftCardService } from "../../../../services" import { Type } from "class-transformer" import { pickBy } from "lodash" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [get] /gift-cards @@ -44,7 +45,7 @@ export default async (req, res) => { const giftCardService: GiftCardService = req.scope.resolve("giftCardService") const [giftCards, count] = await giftCardService.listAndCount( - pickBy(req.filterableFields, (val) => typeof val !== "undefined"), + pickBy(req.filterableFields, (val) => isDefined(val)), req.listConfig ) diff --git a/packages/medusa/src/api/routes/admin/orders/request-return.ts b/packages/medusa/src/api/routes/admin/orders/request-return.ts index a79d5fc7c0..afae0127d6 100644 --- a/packages/medusa/src/api/routes/admin/orders/request-return.ts +++ b/packages/medusa/src/api/routes/admin/orders/request-return.ts @@ -18,6 +18,7 @@ import { OrdersReturnItem } from "../../../../types/orders" import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" import { EntityManager } from "typeorm" +import { isDefined } from "../../../../utils" /** * @oas [post] /orders/{id}/return @@ -140,7 +141,7 @@ export default async (req, res) => { returnObj.shipping_method = value.return_shipping } - if (typeof value.refund !== "undefined" && value.refund < 0) { + if (isDefined(value.refund) && value.refund < 0) { returnObj.refund_amount = 0 } else { if (value.refund && value.refund >= 0) { diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 02d326f7ca..ea7deb266c 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -17,6 +17,7 @@ import { ProductStatus } from "../../../../models" import { Request } from "express" import { Type } from "class-transformer" import { pickBy } from "lodash" +import { isDefined } from "../../../../utils" /** * @oas [get] /price-lists/:id/products @@ -168,7 +169,7 @@ export default async (req: Request, res) => { const [products, count] = await priceListService.listProducts( id, - pickBy(filterableFields, (val) => typeof val !== "undefined"), + pickBy(filterableFields, (val) => isDefined(val)), req.listConfig ) diff --git a/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts b/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts index f4dbafe5e9..a4757cdf54 100644 --- a/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts +++ b/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts @@ -17,6 +17,7 @@ import { ProductTag } from "../../../../models/product-tag" import ProductTagService from "../../../../services/product-tag" import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [get] /product-tags @@ -124,7 +125,7 @@ export default async (req, res) => { take: validated.limit, } - if (typeof validated.order !== "undefined") { + if (isDefined(validated.order)) { let orderField = validated.order if (validated.order.startsWith("-")) { const [, field] = validated.order.split("-") diff --git a/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts b/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts index 8fe0d9260c..84f6e69fbd 100644 --- a/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts +++ b/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts @@ -17,6 +17,7 @@ import { ProductType } from "../../../../models/product-type" import ProductTypeService from "../../../../services/product-type" import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [get] /product-types @@ -125,7 +126,7 @@ export default async (req, res) => { take: validated.limit, } - if (typeof validated.order !== "undefined") { + if (isDefined(validated.order)) { let orderField = validated.order if (validated.order.startsWith("-")) { const [, field] = validated.order.split("-") diff --git a/packages/medusa/src/api/routes/admin/returns/receive-return.ts b/packages/medusa/src/api/routes/admin/returns/receive-return.ts index 10e7633c69..c804a95460 100644 --- a/packages/medusa/src/api/routes/admin/returns/receive-return.ts +++ b/packages/medusa/src/api/routes/admin/returns/receive-return.ts @@ -10,6 +10,7 @@ import { OrderService, ReturnService, SwapService } from "../../../../services" import { EntityManager } from "typeorm" import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [post] /returns/{id}/receive @@ -68,7 +69,7 @@ export default async (req, res) => { await entityManager.transaction(async (manager) => { let refundAmount = validated.refund - if (typeof validated.refund !== "undefined" && validated.refund < 0) { + if (isDefined(validated.refund) && validated.refund < 0) { refundAmount = 0 } diff --git a/packages/medusa/src/api/routes/admin/tax-rates/create-tax-rate.ts b/packages/medusa/src/api/routes/admin/tax-rates/create-tax-rate.ts index 145a0958b6..6ad7a5fb74 100644 --- a/packages/medusa/src/api/routes/admin/tax-rates/create-tax-rate.ts +++ b/packages/medusa/src/api/routes/admin/tax-rates/create-tax-rate.ts @@ -8,6 +8,7 @@ import { TaxRate } from "../../../.." import { TaxRateService } from "../../../../services" import { omit } from "lodash" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [post] /tax-rates @@ -98,15 +99,15 @@ export default async (req, res) => { ) id = created.id - if (typeof value.products !== "undefined") { + if (isDefined(value.products)) { await txRateService.addToProduct(id, value.products) } - if (typeof value.product_types !== "undefined") { + if (isDefined(value.product_types)) { await txRateService.addToProductType(id, value.product_types) } - if (typeof value.shipping_options !== "undefined") { + if (isDefined(value.shipping_options)) { await txRateService.addToShippingOption(id, value.shipping_options) } }) diff --git a/packages/medusa/src/api/routes/admin/tax-rates/update-tax-rate.ts b/packages/medusa/src/api/routes/admin/tax-rates/update-tax-rate.ts index 2ed36a16e0..d7479f7631 100644 --- a/packages/medusa/src/api/routes/admin/tax-rates/update-tax-rate.ts +++ b/packages/medusa/src/api/routes/admin/tax-rates/update-tax-rate.ts @@ -7,6 +7,7 @@ import { TaxRate } from "../../../.." import { TaxRateService } from "../../../../services" import { omit } from "lodash" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [post] /tax-rates/:id @@ -92,11 +93,11 @@ export default async (req, res) => { omit(value, ["products", "product_types", "shipping_options"]) ) - if (typeof value.products !== "undefined") { + if (isDefined(value.products)) { await txRateService.addToProduct(req.params.id, value.products, true) } - if (typeof value.product_types !== "undefined") { + if (isDefined(value.product_types)) { await txRateService.addToProductType( req.params.id, value.product_types, @@ -104,7 +105,7 @@ export default async (req, res) => { ) } - if (typeof value.shipping_options !== "undefined") { + if (isDefined(value.shipping_options)) { await txRateService.addToShippingOption( req.params.id, value.shipping_options, diff --git a/packages/medusa/src/api/routes/admin/tax-rates/utils/get-query-config.ts b/packages/medusa/src/api/routes/admin/tax-rates/utils/get-query-config.ts index caf1bf80e1..36e1952631 100644 --- a/packages/medusa/src/api/routes/admin/tax-rates/utils/get-query-config.ts +++ b/packages/medusa/src/api/routes/admin/tax-rates/utils/get-query-config.ts @@ -2,6 +2,7 @@ import { pick } from "lodash" import { defaultAdminTaxRatesFields, defaultAdminTaxRatesRelations } from "../" import { TaxRate } from "../../../../.." import { FindConfig } from "../../../../../types/common" +import { isDefined } from "../../../../../utils" export function pickByConfig( obj: T | T[], @@ -24,14 +25,14 @@ export function getRetrieveConfig( expand?: string[] ): FindConfig { let includeFields: (keyof TaxRate)[] = [] - if (typeof fields !== "undefined") { + if (isDefined(fields)) { const fieldSet = new Set(fields) fieldSet.add("id") includeFields = Array.from(fieldSet) as (keyof TaxRate)[] } let expandFields: string[] = [] - if (typeof expand !== "undefined") { + if (isDefined(expand)) { expandFields = expand } @@ -51,7 +52,7 @@ export function getListConfig( order?: { [k: symbol]: "DESC" | "ASC" } ): FindConfig { let includeFields: (keyof TaxRate)[] = [] - if (typeof fields !== "undefined") { + if (isDefined(fields)) { const fieldSet = new Set(fields) // Ensure created_at is included, since we are sorting on this fieldSet.add("created_at") @@ -60,7 +61,7 @@ export function getListConfig( } let expandFields: string[] = [] - if (typeof expand !== "undefined") { + if (isDefined(expand)) { expandFields = expand } diff --git a/packages/medusa/src/api/routes/store/carts/create-cart.ts b/packages/medusa/src/api/routes/store/carts/create-cart.ts index 76786e31ef..29cc39a6f6 100644 --- a/packages/medusa/src/api/routes/store/carts/create-cart.ts +++ b/packages/medusa/src/api/routes/store/carts/create-cart.ts @@ -1,7 +1,6 @@ import { CartService, LineItemService, RegionService } from "../../../../services" import { IsArray, - IsBoolean, IsInt, IsNotEmpty, IsOptional, @@ -19,6 +18,7 @@ import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-cha import { Type } from "class-transformer" import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals" import reqIp from "request-ip" +import { isDefined } from "../../../../utils"; /** * @oas [post] /carts @@ -91,9 +91,9 @@ export default async (req, res) => { const entityManager: EntityManager = req.scope.resolve("manager") const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") - let regionId: string - if (typeof validated.region_id !== "undefined") { - regionId = validated.region_id + let regionId!: string + if (isDefined(validated.region_id)) { + regionId = validated.region_id as string } else { const regions = await regionService.list({}) diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index 0ac75ac931..972556952f 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -22,6 +22,7 @@ import { Product } from "../../../../models" import { defaultStoreProductsRelations } from "." import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" import { validator } from "../../../../utils/validator" +import { isDefined } from "../../../../utils" /** * @oas [get] /products @@ -178,7 +179,7 @@ export default async (req, res) => { } const [rawProducts, count] = await productService.listAndCount( - pickBy(filterableFields, (val) => typeof val !== "undefined"), + pickBy(filterableFields, (val) => isDefined(val)), listConfig ) diff --git a/packages/medusa/src/controllers/customers/list-customers.ts b/packages/medusa/src/controllers/customers/list-customers.ts index 9253434308..f787086f81 100644 --- a/packages/medusa/src/controllers/customers/list-customers.ts +++ b/packages/medusa/src/controllers/customers/list-customers.ts @@ -5,6 +5,7 @@ import { CustomerService } from "../../services" import { FindConfig } from "../../types/common" import { validator } from "../../utils/validator" import { Customer } from "../../models/customer" +import { isDefined } from "../../utils" const listAndCount = async ( scope, @@ -33,7 +34,7 @@ const listAndCount = async ( ]) const [customers, count] = await customerService.listAndCount( - pickBy(filterableFields, (val) => typeof val !== "undefined"), + pickBy(filterableFields, (val) => isDefined(val)), listConfig ) diff --git a/packages/medusa/src/loaders/feature-flags/index.ts b/packages/medusa/src/loaders/feature-flags/index.ts index 5343019bd7..60874b43c4 100644 --- a/packages/medusa/src/loaders/feature-flags/index.ts +++ b/packages/medusa/src/loaders/feature-flags/index.ts @@ -5,6 +5,7 @@ import { trackFeatureFlag } from "medusa-telemetry" import { FlagSettings } from "../../types/feature-flags" import { Logger } from "../../types/global" import { FlagRouter } from "../../utils/flag-router" +import { isDefined } from "../../utils" const isTruthy = (val: string | boolean | undefined): boolean => { if (typeof val === "string") { @@ -36,10 +37,10 @@ export default ( flagConfig[flagSettings.key] = isTruthy(flagSettings.default_val) let from - if (typeof process.env[flagSettings.env_key] !== "undefined") { + if (isDefined(process.env[flagSettings.env_key])) { from = "environment" flagConfig[flagSettings.key] = isTruthy(process.env[flagSettings.env_key]) - } else if (typeof projectConfigFlags[flagSettings.key] !== "undefined") { + } else if (isDefined(projectConfigFlags[flagSettings.key])) { from = "project config" flagConfig[flagSettings.key] = isTruthy( projectConfigFlags[flagSettings.key] diff --git a/packages/medusa/src/loaders/services.ts b/packages/medusa/src/loaders/services.ts index a97ea6604c..5b71c0a1b5 100644 --- a/packages/medusa/src/loaders/services.ts +++ b/packages/medusa/src/loaders/services.ts @@ -3,6 +3,7 @@ import path from "path" import { asFunction } from "awilix" import formatRegistrationName from "../utils/format-registration-name" import { ConfigModule, MedusaContainer } from "../types/global" +import { isDefined } from "../utils" type Options = { container: MedusaContainer; @@ -15,7 +16,7 @@ type Options = { */ export default ({ container, configModule, isTest }: Options): void => { const useMock = - typeof isTest !== "undefined" ? isTest : process.env.NODE_ENV === "test" + isDefined(isTest) ? isTest : process.env.NODE_ENV === "test" const corePath = useMock ? "../services/__mocks__/*.js" : "../services/*.js" const coreFull = path.join(__dirname, corePath) diff --git a/packages/medusa/src/loaders/strategies.ts b/packages/medusa/src/loaders/strategies.ts index 9063fcfef6..24028bbb02 100644 --- a/packages/medusa/src/loaders/strategies.ts +++ b/packages/medusa/src/loaders/strategies.ts @@ -5,6 +5,7 @@ import { asFunction, aliasTo } from "awilix" import formatRegistrationName from "../utils/format-registration-name" import { isBatchJobStrategy } from "../interfaces" import { MedusaContainer } from "../types/global" +import { isDefined } from "../utils" type LoaderOptions = { container: MedusaContainer @@ -17,8 +18,7 @@ type LoaderOptions = { * @returns void */ export default ({ container, configModule, isTest }: LoaderOptions): void => { - const useMock = - typeof isTest !== "undefined" ? isTest : process.env.NODE_ENV === "test" + const useMock = isDefined(isTest) ? isTest : process.env.NODE_ENV === "test" const corePath = useMock ? "../strategies/__mocks__/[!__]*.js" diff --git a/packages/medusa/src/repositories/tax-rate.ts b/packages/medusa/src/repositories/tax-rate.ts index 4536a93661..c70406a8db 100644 --- a/packages/medusa/src/repositories/tax-rate.ts +++ b/packages/medusa/src/repositories/tax-rate.ts @@ -16,6 +16,7 @@ import { ShippingTaxRate } from "../models/shipping-tax-rate" import { Product } from "../models/product" import { ShippingMethod } from "../models/shipping-method" import { TaxRateListByConfig } from "../types/tax-rate" +import { isDefined } from "../utils" const resolveableFields = [ "product_count", @@ -30,7 +31,7 @@ export class TaxRateRepository extends Repository { const cleanOptions = findOptions const resolverFields: string[] = [] - if (typeof findOptions.select !== "undefined") { + if (isDefined(findOptions.select)) { let selectableCols: (keyof TaxRate)[] = [] for (const k of findOptions.select) { if (!resolveableFields.includes(k)) { @@ -235,7 +236,7 @@ export class TaxRateRepository extends Repository { .leftJoin(Product, "prod", "prod.type_id = pttr.product_type_id") .where("prod.id = :productId", { productId }) - if (typeof config.region_id !== "undefined") { + if (isDefined(config.region_id)) { productRates.andWhere("txr.region_id = :regionId", { regionId: config.region_id, }) diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 8caa2acdb4..96f2c06610 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -27,7 +27,7 @@ import { LineItemUpdate, } from "../types/cart" import { AddressPayload, FindConfig, TotalField } from "../types/common" -import { buildQuery, setMetadata, validateId } from "../utils" +import { buildQuery, isDefined, setMetadata, validateId } from "../utils" import CustomShippingOptionService from "./custom-shipping-option" import CustomerService from "./customer" import DiscountService from "./discount" @@ -423,10 +423,7 @@ class CartService extends TransactionBaseService { ] for (const remainingField of remainingFields) { - if ( - typeof data[remainingField] !== "undefined" && - remainingField !== "object" - ) { + if (isDefined(data[remainingField]) && remainingField !== "object") { const key = remainingField as string rawCart[key] = data[remainingField] } @@ -448,7 +445,7 @@ class CartService extends TransactionBaseService { salesChannelId?: string ): Promise { let salesChannel: SalesChannel - if (typeof salesChannelId !== "undefined") { + if (isDefined(salesChannelId)) { salesChannel = await this.salesChannelService_ .withTransaction(this.manager_) .retrieve(salesChannelId) @@ -858,7 +855,7 @@ class CartService extends TransactionBaseService { if (data.customer_id) { await this.updateCustomerId_(cart, data.customer_id) } else { - if (typeof data.email !== "undefined") { + if (isDefined(data.email)) { const customer = await this.createOrFetchUserFromEmail_(data.email) cart.customer = customer cart.customer_id = customer.id @@ -866,14 +863,11 @@ class CartService extends TransactionBaseService { } } - if ( - typeof data.customer_id !== "undefined" || - typeof data.region_id !== "undefined" - ) { + if (isDefined(data.customer_id) || isDefined(data.region_id)) { await this.updateUnitPrices_(cart, data.region_id, data.customer_id) } - if (typeof data.region_id !== "undefined") { + if (isDefined(data.region_id)) { const shippingAddress = typeof data.shipping_address !== "string" ? data.shipping_address @@ -902,7 +896,7 @@ class CartService extends TransactionBaseService { this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) ) { if ( - typeof data.sales_channel_id !== "undefined" && + isDefined(data.sales_channel_id) && data.sales_channel_id != cart.sales_channel_id ) { await this.onSalesChannelChange(cart, data.sales_channel_id) @@ -910,7 +904,7 @@ class CartService extends TransactionBaseService { } } - if (typeof data.discounts !== "undefined") { + if (isDefined(data.discounts)) { const previousDiscounts = [...cart.discounts] cart.discounts.length = 0 diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index 82340a5c26..b68ac78209 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -25,7 +25,7 @@ import { LineItemRepository } from "../repositories/line-item" import { MedusaError } from "medusa-core-utils" import { ShippingMethodRepository } from "../repositories/shipping-method" import { TransactionBaseService } from "../interfaces" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isDefined, setMetadata } from "../utils" import { FindConfig } from "../types/common" import { CreateClaimInput, UpdateClaimInput } from "../types/claim" @@ -336,7 +336,7 @@ export default class ClaimService extends TransactionBaseService< } let newItems: LineItem[] = [] - if (typeof additional_items !== "undefined") { + if (isDefined(additional_items)) { for (const item of additional_items) { await this.inventoryService_ .withTransaction(transactionManager) diff --git a/packages/medusa/src/services/customer-group.ts b/packages/medusa/src/services/customer-group.ts index 9b46c071da..a094eeb4a7 100644 --- a/packages/medusa/src/services/customer-group.ts +++ b/packages/medusa/src/services/customer-group.ts @@ -9,6 +9,7 @@ import { CustomerGroupUpdate, FilterableCustomerGroupProps, } from "../types/customer-groups" +import { isDefined } from "../utils" import { formatException } from "../utils/exception-formatter" type CustomerGroupConstructorProps = { @@ -173,12 +174,12 @@ class CustomerGroupService extends BaseService { const customerGroup = await this.retrieve(customerGroupId) for (const key in properties) { - if (typeof properties[key] !== "undefined") { + if (isDefined(properties[key])) { customerGroup[key] = properties[key] } } - if (typeof metadata !== "undefined") { + if (isDefined(metadata)) { customerGroup.metadata = this.setMetadata_(customerGroup, metadata) } return await cgRepo.save(customerGroup) diff --git a/packages/medusa/src/services/customer.ts b/packages/medusa/src/services/customer.ts index b2f29a70fd..8487bfc9a0 100644 --- a/packages/medusa/src/services/customer.ts +++ b/packages/medusa/src/services/customer.ts @@ -9,7 +9,7 @@ import { AddressRepository } from "../repositories/address" import { CustomerRepository } from "../repositories/customer" import { AddressCreatePayload, FindConfig, Selector } from "../types/common" import { CreateCustomerInput, UpdateCustomerInput } from "../types/customers" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isDefined, setMetadata } from "../utils" import { formatException } from "../utils/exception-formatter" import EventBusService from "./event-bus" @@ -324,7 +324,7 @@ class CustomerService extends TransactionBaseService { if ("billing_address_id" in update || "billing_address" in update) { const address = billing_address_id || billing_address - if (typeof address !== "undefined") { + if (isDefined(address)) { await this.updateBillingAddress_(customer, address) } } @@ -395,7 +395,7 @@ class CustomerService extends TransactionBaseService { address.country_code = address.country_code?.toLowerCase() - if (typeof address?.id !== "undefined") { + if (isDefined(address?.id)) { customer.billing_address_id = address.id } else { if (customer.billing_address_id) { diff --git a/packages/medusa/src/services/fulfillment.ts b/packages/medusa/src/services/fulfillment.ts index 0756f335c7..9a116b595f 100644 --- a/packages/medusa/src/services/fulfillment.ts +++ b/packages/medusa/src/services/fulfillment.ts @@ -13,7 +13,7 @@ import { FulfillmentItemPartition, FulFillmentItemType, } from "../types/fulfillment" -import { buildQuery } from "../utils" +import { buildQuery, isDefined } from "../utils" import FulfillmentProviderService from "./fulfillment-provider" import LineItemService from "./line-item" import TotalsService from "./totals" @@ -336,7 +336,7 @@ class FulfillmentService extends TransactionBaseService { trackingLinkRepo.create(tl) ) - if (typeof no_notification !== "undefined") { + if (isDefined(no_notification)) { fulfillment.no_notification = no_notification } diff --git a/packages/medusa/src/services/gift-card.ts b/packages/medusa/src/services/gift-card.ts index b115b71400..3da6c288e1 100644 --- a/packages/medusa/src/services/gift-card.ts +++ b/packages/medusa/src/services/gift-card.ts @@ -17,7 +17,7 @@ import { CreateGiftCardTransactionInput, UpdateGiftCardInput, } from "../types/gift-card" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isDefined, setMetadata } from "../utils" import RegionService from "./region" type InjectedDependencies = { @@ -89,7 +89,7 @@ class GiftCardService extends TransactionBaseService { const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) let q: string | undefined - if (typeof selector.q !== "undefined") { + if (isDefined(selector.q)) { q = selector.q delete selector.q } @@ -118,7 +118,7 @@ class GiftCardService extends TransactionBaseService { const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) let q: string | undefined - if (typeof selector.q !== "undefined") { + if (isDefined(selector.q)) { q = selector.q delete selector.q } @@ -255,7 +255,7 @@ class GiftCardService extends TransactionBaseService { giftCard.metadata = setMetadata(giftCard, metadata) } - if (typeof balance !== "undefined") { + if (isDefined(balance)) { if (balance < 0 || giftCard.value < balance) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 9715c15c45..311e2ce117 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -27,6 +27,7 @@ import { ProductVariantPrice, UpdateProductVariantInput, } from "../types/product-variant" +import { isDefined } from "../utils" /** * Provides layer to manipulate product variants. @@ -751,7 +752,7 @@ class ProductVariantService extends BaseService { config: FindConfig ): { query: FindWithRelationsOptions; relations: string[]; q?: string } { let q: string | undefined - if (typeof selector.q !== "undefined") { + if (isDefined(selector.q)) { q = selector.q delete selector.q } diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 0faa176684..3369949387 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -29,7 +29,7 @@ import { ProductOptionInput, UpdateProductInput, } from "../types/product" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isDefined, setMetadata } from "../utils" import { formatException } from "../utils/exception-formatter" import EventBusService from "./event-bus" @@ -393,7 +393,7 @@ class ProductService extends TransactionBaseService< if ( this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) ) { - if (typeof salesChannels !== "undefined") { + if (isDefined(salesChannels)) { product.sales_channels = [] if (salesChannels?.length) { const salesChannelIds = salesChannels?.map((sc) => sc.id) @@ -464,11 +464,11 @@ class ProductService extends TransactionBaseService< if ( this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) ) { - if (typeof update.sales_channels !== "undefined") { + if (isDefined(update.sales_channels)) { relations.push("sales_channels") } } else { - if (typeof update.sales_channels !== "undefined") { + if (isDefined(update.sales_channels)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "the property sales_channels should no appears as part of the payload" @@ -513,7 +513,7 @@ class ProductService extends TransactionBaseService< if ( this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) ) { - if (typeof salesChannels !== "undefined") { + if (isDefined(salesChannels)) { product.sales_channels = [] if (salesChannels?.length) { const salesChannelIds = salesChannels?.map((sc) => sc.id) diff --git a/packages/medusa/src/services/return.js b/packages/medusa/src/services/return.js index 81696b2472..5de7d227a8 100644 --- a/packages/medusa/src/services/return.js +++ b/packages/medusa/src/services/return.js @@ -1,5 +1,6 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { isDefined } from "../utils" /** * Handles Returns @@ -357,7 +358,7 @@ class ReturnService extends BaseService { ) let toRefund = data.refund_amount - if (typeof toRefund !== "undefined") { + if (isDefined(toRefund)) { // Merchant wants to do a custom refund amount; we check if amount is // refundable const refundable = order.refundable_amount diff --git a/packages/medusa/src/services/shipping-option.ts b/packages/medusa/src/services/shipping-option.ts index 358d3e65a4..0eef3b2ce5 100644 --- a/packages/medusa/src/services/shipping-option.ts +++ b/packages/medusa/src/services/shipping-option.ts @@ -19,7 +19,7 @@ import { UpdateShippingOptionInput, CreateShippingOptionInput, } from "../types/shipping-options" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isDefined, setMetadata } from "../utils" import FulfillmentProviderService from "./fulfillment-provider" import RegionService from "./region" @@ -262,7 +262,7 @@ class ShippingOptionService extends TransactionBaseService { if (next === "cart") { @@ -140,7 +141,7 @@ class SwapService extends BaseService { cartRelations = cartRels let foundCartId = false - if (typeof select !== "undefined") { + if (isDefined(select)) { const [swapSelects, cartSels] = select.reduce( (acc, next) => { if (next.startsWith("cart.")) { diff --git a/packages/medusa/src/services/tax-rate.ts b/packages/medusa/src/services/tax-rate.ts index 5b7829be22..c0aed1d12d 100644 --- a/packages/medusa/src/services/tax-rate.ts +++ b/packages/medusa/src/services/tax-rate.ts @@ -16,6 +16,7 @@ import { TaxRateListByConfig, UpdateTaxRateInput, } from "../types/tax-rate" +import { isDefined } from "../utils" class TaxRateService extends BaseService { private manager_: EntityManager @@ -122,7 +123,7 @@ class TaxRateService extends BaseService { const taxRate = await this.retrieve(id) for (const [k, v] of Object.entries(data)) { - if (typeof v !== "undefined") { + if (isDefined(v)) { taxRate[k] = v } } diff --git a/packages/medusa/src/services/totals.ts b/packages/medusa/src/services/totals.ts index b21d4fe702..8addac9e7a 100644 --- a/packages/medusa/src/services/totals.ts +++ b/packages/medusa/src/services/totals.ts @@ -23,6 +23,7 @@ import { } from "../types/totals" import TaxProviderService from "./tax-provider" import { EntityManager } from "typeorm" +import { isDefined } from "../utils" type ShippingMethodTotals = { price: number @@ -315,8 +316,8 @@ class TotalsService extends TransactionBaseService { let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[] if (isOrder(cartOrOrder)) { - const taxLinesJoined = cartOrOrder.items.every( - (i) => typeof i.tax_lines !== "undefined" + const taxLinesJoined = cartOrOrder.items.every((i) => + isDefined(i.tax_lines) ) if (!taxLinesJoined) { throw new MedusaError( diff --git a/packages/medusa/src/strategies/price-selection.ts b/packages/medusa/src/strategies/price-selection.ts index d128169f79..68e635928b 100644 --- a/packages/medusa/src/strategies/price-selection.ts +++ b/packages/medusa/src/strategies/price-selection.ts @@ -7,6 +7,7 @@ import { } from "../interfaces/price-selection-strategy" import { MoneyAmountRepository } from "../repositories/money-amount" import { EntityManager } from "typeorm" +import { isDefined } from "../utils" class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { private moneyAmountRepository_: typeof MoneyAmountRepository @@ -103,8 +104,7 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { } const isValidQuantity = (price, quantity): boolean => - (typeof quantity !== "undefined" && - isValidPriceWithQuantity(price, quantity)) || + (isDefined(quantity) && isValidPriceWithQuantity(price, quantity)) || (typeof quantity === "undefined" && isValidPriceWithoutQuantity(price)) const isValidPriceWithoutQuantity = (price): boolean => diff --git a/packages/medusa/src/utils/get-query-config.ts b/packages/medusa/src/utils/get-query-config.ts index 841a4981b0..8bc8e6b02c 100644 --- a/packages/medusa/src/utils/get-query-config.ts +++ b/packages/medusa/src/utils/get-query-config.ts @@ -2,6 +2,7 @@ import { pick } from "lodash" import { FindConfig, QueryConfig, RequestQueryFields } from "../types/common" import { MedusaError } from "medusa-core-utils/dist" import { BaseEntity } from "../interfaces/models/base-entity" +import { isDefined } from "." export function pickByConfig( obj: TModel | TModel[], @@ -26,14 +27,14 @@ export function getRetrieveConfig( expand?: string[] ): FindConfig { let includeFields: (keyof TModel)[] = [] - if (typeof fields !== "undefined") { + if (isDefined(fields)) { includeFields = Array.from(new Set([...fields, "id"])).map((field) => typeof field === "string" ? field.trim() : field ) as (keyof TModel)[] } let expandFields: string[] = [] - if (typeof expand !== "undefined") { + if (isDefined(expand)) { expandFields = expand.map((expandRelation) => expandRelation.trim()) } @@ -53,7 +54,7 @@ export function getListConfig( order?: { [k: symbol]: "DESC" | "ASC" } ): FindConfig { let includeFields: (keyof TModel)[] = [] - if (typeof fields !== "undefined") { + if (isDefined(fields)) { const fieldSet = new Set(fields) // Ensure created_at is included, since we are sorting on this fieldSet.add("created_at") @@ -62,7 +63,7 @@ export function getListConfig( } let expandFields: string[] = [] - if (typeof expand !== "undefined") { + if (isDefined(expand)) { expandFields = expand } @@ -96,7 +97,7 @@ export function prepareListQuery< } let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined - if (typeof order !== "undefined") { + if (isDefined(order)) { let orderField = order if (order.startsWith("-")) { const [, field] = order.split("-") diff --git a/packages/medusa/src/utils/index.ts b/packages/medusa/src/utils/index.ts index 6b1988de67..d56c80bac0 100644 --- a/packages/medusa/src/utils/index.ts +++ b/packages/medusa/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./set-metadata" export * from "./validate-id" export * from "./generate-entity-id" export * from "./remove-undefined-properties" +export * from "./is-defined" diff --git a/packages/medusa/src/utils/is-defined.ts b/packages/medusa/src/utils/is-defined.ts new file mode 100644 index 0000000000..2d4da48450 --- /dev/null +++ b/packages/medusa/src/utils/is-defined.ts @@ -0,0 +1,3 @@ +export function isDefined(val: T): val is (T extends undefined ? never : T) { + return typeof val !== "undefined" +} diff --git a/packages/medusa/src/utils/remove-undefined-properties.ts b/packages/medusa/src/utils/remove-undefined-properties.ts index 0898238880..010f9c3588 100644 --- a/packages/medusa/src/utils/remove-undefined-properties.ts +++ b/packages/medusa/src/utils/remove-undefined-properties.ts @@ -1,3 +1,5 @@ +import { isDefined } from "./is-defined"; + export function removeUndefinedProperties(inputObj: T): T { const removeProperties = (obj: T) => { const res = {} as T @@ -17,13 +19,13 @@ export function removeUndefinedProperties(inputObj: T): T { } function removeUndefinedDeeply(input: unknown): any { - if (typeof input !== "undefined") { + if (isDefined(input)) { if (input === null || input === "null") { return null } else if (Array.isArray(input)) { return input.map((item) => { return removeUndefinedDeeply(item) - }).filter(v => typeof v !== "undefined") + }).filter(v => isDefined(v)) } else if (Object.prototype.toString.call(input) === '[object Date]') { return input } else if (typeof input === "object") { From cbe2b7f687923aecdf34db31726c048ef1f8db21 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 11 Aug 2022 22:18:11 +0200 Subject: [PATCH 14/34] chore(medusa): Remove intepestive services re instanciation in loop (#2036) * chore(medusa): Renove intepestive services re instanciation in loop * test(medusa): Fix missing deps * fix(medusa): Missing await --- .../medusa/src/services/__tests__/order.js | 2 + .../src/services/__tests__/price-list.js | 2 + packages/medusa/src/services/cart.ts | 20 ++- packages/medusa/src/services/claim.ts | 155 +++++++++--------- packages/medusa/src/services/draft-order.ts | 64 ++++---- packages/medusa/src/services/fulfillment.ts | 6 +- packages/medusa/src/services/order.ts | 65 ++++---- packages/medusa/src/services/price-list.ts | 5 +- packages/medusa/src/services/product.ts | 10 +- packages/medusa/src/services/return.js | 15 +- .../medusa/src/services/shipping-profile.ts | 13 +- packages/medusa/src/services/swap.js | 76 +++++---- 12 files changed, 240 insertions(+), 193 deletions(-) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index 77d4a9d86b..6198601231 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1,6 +1,7 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import OrderService from "../order" import { InventoryServiceMock } from "../__mocks__/inventory" +import { LineItemServiceMock } from "../__mocks__/line-item"; describe("OrderService", () => { const totalsService = { @@ -520,6 +521,7 @@ describe("OrderService", () => { manager: MockManager, orderRepository: orderRepo, eventBusService, + lineItemService: LineItemServiceMock }) beforeEach(async () => { diff --git a/packages/medusa/src/services/__tests__/price-list.js b/packages/medusa/src/services/__tests__/price-list.js index d794530f4f..588085e003 100644 --- a/packages/medusa/src/services/__tests__/price-list.js +++ b/packages/medusa/src/services/__tests__/price-list.js @@ -2,6 +2,7 @@ import { MedusaError } from "medusa-core-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import PriceListService from "../price-list" import { MoneyAmountRepository } from "../../repositories/money-amount" +import { RegionServiceMock } from "../__mocks__/region"; const priceListRepository = MockRepository({ findOne: (q) => { @@ -129,6 +130,7 @@ describe("PriceListService", () => { customerGroupService, priceListRepository, moneyAmountRepository: updateRelatedMoneyAmountRepository, + regionService: RegionServiceMock }) it("update only existing price lists and related money amount", async () => { diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 96f2c06610..ed5e142631 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1736,14 +1736,17 @@ class CartService extends TransactionBaseService { const methods = [newShippingMethod] if (shipping_methods?.length) { + const shippingOptionServiceTx = + this.shippingOptionService_.withTransaction(transactionManager) + for (const shippingMethod of shipping_methods) { if ( shippingMethod.shipping_option.profile_id === newShippingMethod.shipping_option.profile_id ) { - await this.shippingOptionService_ - .withTransaction(transactionManager) - .deleteShippingMethods(shippingMethod) + await shippingOptionServiceTx.deleteShippingMethods( + shippingMethod + ) } else { methods.push(shippingMethod) } @@ -1751,13 +1754,14 @@ class CartService extends TransactionBaseService { } if (cart.items?.length) { + const lineItemServiceTx = + this.lineItemService_.withTransaction(transactionManager) + await Promise.all( cart.items.map(async (item) => { - return this.lineItemService_ - .withTransaction(transactionManager) - .update(item.id, { - has_shipping: this.validateLineItemShipping_(methods, item), - }) + return lineItemServiceTx.update(item.id, { + has_shipping: this.validateLineItemShipping_(methods, item), + }) }) ) } diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index b68ac78209..1253d8acb9 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -150,32 +150,29 @@ export default class ClaimService extends TransactionBaseService< } if (shipping_methods) { + const shippingOptionServiceTx = + this.shippingOptionService_.withTransaction(transactionManager) + for (const m of claim.shipping_methods) { - await this.shippingOptionService_ - .withTransaction(transactionManager) - .updateShippingMethod(m.id, { - claim_order_id: null, - }) + await shippingOptionServiceTx.updateShippingMethod(m.id, { + claim_order_id: null, + }) } for (const method of shipping_methods) { if (method.id) { - await this.shippingOptionService_ - .withTransaction(transactionManager) - .updateShippingMethod(method.id, { - claim_order_id: claim.id, - }) + await shippingOptionServiceTx.updateShippingMethod(method.id, { + claim_order_id: claim.id, + }) } else { - await this.shippingOptionService_ - .withTransaction(transactionManager) - .createShippingMethod( - method.option_id as string, - (method as any).data, - { - claim_order_id: claim.id, - price: method.price, - } - ) + await shippingOptionServiceTx.createShippingMethod( + method.option_id as string, + (method as any).data, + { + claim_order_id: claim.id, + price: method.price, + } + ) } } } @@ -186,11 +183,12 @@ export default class ClaimService extends TransactionBaseService< } if (claim_items) { + const claimItemServiceTx = + this.claimItemService_.withTransaction(transactionManager) + for (const i of claim_items) { if (i.id) { - await this.claimItemService_ - .withTransaction(transactionManager) - .update(i.id, i) + await claimItemServiceTx.update(i.id, i) } } } @@ -235,12 +233,13 @@ export default class ClaimService extends TransactionBaseService< ...rest } = data + const lineItemServiceTx = + this.lineItemService_.withTransaction(transactionManager) + for (const item of claim_items) { - const line = await this.lineItemService_ - .withTransaction(transactionManager) - .retrieve(item.item_id, { - relations: ["order", "swap", "claim_order", "tax_lines"], - }) + const line = await lineItemServiceTx.retrieve(item.item_id, { + relations: ["order", "swap", "claim_order", "tax_lines"], + }) if ( line.order?.canceled_at || @@ -337,24 +336,31 @@ export default class ClaimService extends TransactionBaseService< let newItems: LineItem[] = [] if (isDefined(additional_items)) { + const inventoryServiceTx = + this.inventoryService_.withTransaction(transactionManager) + for (const item of additional_items) { - await this.inventoryService_ - .withTransaction(transactionManager) - .confirmInventory(item.variant_id, item.quantity) + await inventoryServiceTx.confirmInventory( + item.variant_id, + item.quantity + ) } newItems = await Promise.all( additional_items.map((i) => - this.lineItemService_ - .withTransaction(transactionManager) - .generate(i.variant_id, order.region_id, i.quantity) + lineItemServiceTx.generate( + i.variant_id, + order.region_id, + i.quantity + ) ) ) for (const newItem of newItems) { - await this.inventoryService_ - .withTransaction(transactionManager) - .adjustInventory(newItem.variant_id, -newItem.quantity) + await inventoryServiceTx.adjustInventory( + newItem.variant_id, + -newItem.quantity + ) } } @@ -378,46 +384,44 @@ export default class ClaimService extends TransactionBaseService< if (result.additional_items && result.additional_items.length) { const calcContext = this.totalsService_.getCalculationContext(order) - const lineItems = await this.lineItemService_ - .withTransaction(transactionManager) - .list({ - id: result.additional_items.map((i) => i.id), - }) + const lineItems = await lineItemServiceTx.list({ + id: result.additional_items.map((i) => i.id), + }) await this.taxProviderService_ .withTransaction(transactionManager) .createTaxLines(lineItems, calcContext) } if (shipping_methods) { + const shippingOptionServiceTx = + this.shippingOptionService_.withTransaction(transactionManager) + for (const method of shipping_methods) { if (method.id) { - await this.shippingOptionService_ - .withTransaction(transactionManager) - .updateShippingMethod(method.id, { - claim_order_id: result.id, - }) + await shippingOptionServiceTx.updateShippingMethod(method.id, { + claim_order_id: result.id, + }) } else { - await this.shippingOptionService_ - .withTransaction(transactionManager) - .createShippingMethod( - method.option_id as string, - (method as any).data, - { - claim_order_id: result.id, - price: method.price, - } - ) + await shippingOptionServiceTx.createShippingMethod( + method.option_id as string, + (method as any).data, + { + claim_order_id: result.id, + price: method.price, + } + ) } } } + const claimItemServiceTx = + this.claimItemService_.withTransaction(transactionManager) + for (const ci of claim_items) { - await this.claimItemService_ - .withTransaction(transactionManager) - .create({ - ...ci, - claim_order_id: result.id, - }) + await claimItemServiceTx.create({ + ...ci, + claim_order_id: result.id, + }) } if (return_shipping) { @@ -584,14 +588,14 @@ export default class ClaimService extends TransactionBaseService< ) const claimOrder = await claimRepo.save(claim) + const eventBusTx = this.eventBus_.withTransaction(transactionManager) + for (const fulfillment of fulfillments) { - await this.eventBus_ - .withTransaction(transactionManager) - .emit(ClaimService.Events.FULFILLMENT_CREATED, { - id: id, - fulfillment_id: fulfillment.id, - no_notification: claim.no_notification, - }) + await eventBusTx.emit(ClaimService.Events.FULFILLMENT_CREATED, { + id: id, + fulfillment_id: fulfillment.id, + no_notification: claim.no_notification, + }) } return claimOrder @@ -708,6 +712,9 @@ export default class ClaimService extends TransactionBaseService< claim.fulfillment_status = ClaimFulfillmentStatus.SHIPPED + const lineItemServiceTx = + this.lineItemService_.withTransaction(transactionManager) + for (const additionalItem of claim.additional_items) { const shipped = shipment.items.find( (si) => si.item_id === additionalItem.id @@ -715,11 +722,9 @@ export default class ClaimService extends TransactionBaseService< if (shipped) { const shippedQty = (additionalItem.shipped_quantity || 0) + shipped.quantity - await this.lineItemService_ - .withTransaction(transactionManager) - .update(additionalItem.id, { - shipped_quantity: shippedQty, - }) + await lineItemServiceTx.update(additionalItem.id, { + shipped_quantity: shippedQty, + }) if (shippedQty !== additionalItem.quantity) { claim.fulfillment_status = diff --git a/packages/medusa/src/services/draft-order.ts b/packages/medusa/src/services/draft-order.ts index cffb387a0b..e519aab001 100644 --- a/packages/medusa/src/services/draft-order.ts +++ b/packages/medusa/src/services/draft-order.ts @@ -266,20 +266,22 @@ class DraftOrderService extends TransactionBaseService { const { shipping_methods, no_notification_order, items, ...rawCart } = data + const cartServiceTx = + this.cartService_.withTransaction(transactionManager) + if (rawCart.discounts) { const { discounts } = rawCart rawCart.discounts = [] for (const { code } of discounts) { - await this.cartService_ - .withTransaction(transactionManager) - .applyDiscount(rawCart as Cart, code) + await cartServiceTx.applyDiscount(rawCart as Cart, code) } } - const createdCart = await this.cartService_ - .withTransaction(transactionManager) - .create({ type: CartType.DRAFT_ORDER, ...rawCart }) + const createdCart = await cartServiceTx.create({ + type: CartType.DRAFT_ORDER, + ...rawCart, + }) const draftOrder = draftOrderRepo.create({ cart_id: createdCart.id, @@ -293,22 +295,26 @@ class DraftOrderService extends TransactionBaseService { id: result.id, }) + const lineItemServiceTx = + this.lineItemService_.withTransaction(transactionManager) + for (const item of items) { if (item.variant_id) { - const line = await this.lineItemService_ - .withTransaction(transactionManager) - .generate(item.variant_id, data.region_id, item.quantity, { + const line = await lineItemServiceTx.generate( + item.variant_id, + data.region_id, + item.quantity, + { metadata: item?.metadata || {}, unit_price: item.unit_price, cart: createdCart, - }) + } + ) - await this.lineItemService_ - .withTransaction(transactionManager) - .create({ - ...line, - cart_id: createdCart.id, - }) + await lineItemServiceTx.create({ + ...line, + cart_id: createdCart.id, + }) } else { let price if (typeof item.unit_price === `undefined` || item.unit_price < 0) { @@ -318,23 +324,23 @@ class DraftOrderService extends TransactionBaseService { } // custom line items can be added to a draft order - await this.lineItemService_ - .withTransaction(transactionManager) - .create({ - cart_id: createdCart.id, - has_shipping: true, - title: item.title || "Custom item", - allow_discounts: false, - unit_price: price, - quantity: item.quantity, - }) + await lineItemServiceTx.create({ + cart_id: createdCart.id, + has_shipping: true, + title: item.title || "Custom item", + allow_discounts: false, + unit_price: price, + quantity: item.quantity, + }) } } for (const method of shipping_methods) { - await this.cartService_ - .withTransaction(transactionManager) - .addShippingMethod(createdCart.id, method.option_id, method.data) + await cartServiceTx.addShippingMethod( + createdCart.id, + method.option_id, + method.data + ) } return result diff --git a/packages/medusa/src/services/fulfillment.ts b/packages/medusa/src/services/fulfillment.ts index 9a116b595f..89a97b770d 100644 --- a/packages/medusa/src/services/fulfillment.ts +++ b/packages/medusa/src/services/fulfillment.ts @@ -274,12 +274,12 @@ class FulfillmentService extends TransactionBaseService { fulfillment.canceled_at = new Date() - const lineItemService = this.lineItemService_.withTransaction(manager) + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) for (const fItem of fulfillment.items) { - const item = await lineItemService.retrieve(fItem.item_id) + const item = await lineItemServiceTx.retrieve(fItem.item_id) const fulfilledQuantity = item.fulfilled_quantity - fItem.quantity - await lineItemService.update(item.id, { + await lineItemServiceTx.update(item.id, { fulfilled_quantity: fulfilledQuantity, }) } diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index f99105e0bc..93115a1b9a 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -479,10 +479,10 @@ class OrderService extends TransactionBaseService { */ async createFromCart(cartId: string): Promise { return await this.atomicPhase_(async (manager) => { - const cartService = this.cartService_.withTransaction(manager) - const inventoryService = this.inventoryService_.withTransaction(manager) + const cartServiceTx = this.cartService_.withTransaction(manager) + const inventoryServiceTx = this.inventoryService_.withTransaction(manager) - const cart = await cartService.retrieve(cartId, { + const cart = await cartServiceTx.retrieve(cartId, { select: ["subtotal", "total"], relations: [ "region", @@ -506,7 +506,7 @@ class OrderService extends TransactionBaseService { for (const item of cart.items) { try { - await inventoryService.confirmInventory( + await inventoryServiceTx.confirmInventory( item.variant_id, item.quantity ) @@ -516,7 +516,7 @@ class OrderService extends TransactionBaseService { .withTransaction(manager) .cancelPayment(payment) } - await cartService.update(cart.id, { payment_authorized_at: null }) + await cartServiceTx.update(cart.id, { payment_authorized_at: null }) throw err } } @@ -618,13 +618,16 @@ class OrderService extends TransactionBaseService { .updateShippingMethod(method.id, { order_id: result.id }) } - const lineItemService = this.lineItemService_.withTransaction(manager) + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) for (const item of cart.items) { - await lineItemService.update(item.id, { order_id: result.id }) + await lineItemServiceTx.update(item.id, { order_id: result.id }) } for (const item of cart.items) { - await inventoryService.adjustInventory(item.variant_id, -item.quantity) + await inventoryServiceTx.adjustInventory( + item.variant_id, + -item.quantity + ) } await this.eventBus_ @@ -634,7 +637,7 @@ class OrderService extends TransactionBaseService { no_notification: result.no_notification, }) - await cartService.update(cart.id, { completed_at: new Date() }) + await cartServiceTx.update(cart.id, { completed_at: new Date() }) return result }) @@ -697,6 +700,8 @@ class OrderService extends TransactionBaseService { no_notification: evaluatedNoNotification, }) + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + order.fulfillment_status = FulfillmentStatus.SHIPPED for (const item of order.items) { const shipped = shipmentRes.items.find((si) => si.item_id === item.id) @@ -706,7 +711,7 @@ class OrderService extends TransactionBaseService { order.fulfillment_status = FulfillmentStatus.PARTIALLY_SHIPPED } - await this.lineItemService_.withTransaction(manager).update(item.id, { + await lineItemServiceTx.update(item.id, { shipped_quantity: shippedQty, }) } else { @@ -839,6 +844,9 @@ class OrderService extends TransactionBaseService { .withTransaction(manager) .createShippingMethod(optionId, data ?? {}, { order, ...config }) + const shippingOptionServiceTx = + this.shippingOptionService_.withTransaction(manager) + const methods = [newMethod] if (shipping_methods.length) { for (const sm of shipping_methods) { @@ -846,9 +854,7 @@ class OrderService extends TransactionBaseService { sm.shipping_option.profile_id === newMethod.shipping_option.profile_id ) { - await this.shippingOptionService_ - .withTransaction(manager) - .deleteShippingMethods(sm) + await shippingOptionServiceTx.deleteShippingMethods(sm) } else { methods.push(sm) } @@ -927,9 +933,10 @@ class OrderService extends TransactionBaseService { order.no_notification = no_notification ?? false } + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) if (update.items) { for (const item of items as LineItem[]) { - await this.lineItemService_.withTransaction(manager).create({ + await lineItemServiceTx.create({ ...item, order_id: orderId, }) @@ -1004,16 +1011,15 @@ class OrderService extends TransactionBaseService { throwErrorIf(order.swaps, notCanceled, "swaps") throwErrorIf(order.claims, notCanceled, "claims") + const inventoryServiceTx = this.inventoryService_.withTransaction(manager) for (const item of order.items) { - await this.inventoryService_ - .withTransaction(manager) - .adjustInventory(item.variant_id, item.quantity) + await inventoryServiceTx.adjustInventory(item.variant_id, item.quantity) } + const paymentProviderServiceTx = + this.paymentProviderService_.withTransaction(manager) for (const p of order.payments) { - await this.paymentProviderService_ - .withTransaction(manager) - .cancelPayment(p) + await paymentProviderServiceTx.cancelPayment(p) } order.status = OrderStatus.CANCELED @@ -1051,11 +1057,13 @@ class OrderService extends TransactionBaseService { ) } + const paymentProviderServiceTx = + this.paymentProviderService_.withTransaction(manager) + const payments: Payment[] = [] for (const p of order.payments) { if (p.captured_at === null) { - const result = await this.paymentProviderService_ - .withTransaction(manager) + const result = await paymentProviderServiceTx .capturePayment(p) .catch((err) => { this.eventBus_ @@ -1251,14 +1259,13 @@ class OrderService extends TransactionBaseService { const evaluatedNoNotification = no_notification !== undefined ? no_notification : order.no_notification + const eventBusTx = this.eventBus_.withTransaction(manager) for (const fulfillment of fulfillments) { - await this.eventBus_ - .withTransaction(manager) - .emit(OrderService.Events.FULFILLMENT_CREATED, { - id: orderId, - fulfillment_id: fulfillment.id, - no_notification: evaluatedNoNotification, - }) + await eventBusTx.emit(OrderService.Events.FULFILLMENT_CREATED, { + id: orderId, + fulfillment_id: fulfillment.id, + no_notification: evaluatedNoNotification, + }) } return result diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index bac06de9c3..f005c0c1fb 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -476,11 +476,10 @@ class PriceListService extends TransactionBaseService { >(prices: T[]): Promise { const prices_: typeof prices = [] + const regionServiceTx = this.regionService_.withTransaction(this.manager_) for (const p of prices) { if (p.region_id) { - const region = await this.regionService_ - .withTransaction(this.manager_) - .retrieve(p.region_id) + const region = await regionServiceTx.retrieve(p.region_id) p.currency_code = region.currency_code } diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 3369949387..7346a4579d 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -658,10 +658,14 @@ class ProductService extends TransactionBaseService< await productOptionRepo.save(option) + const productVariantServiceTx = + this.productVariantService_.withTransaction(manager) for (const variant of product.variants) { - this.productVariantService_ - .withTransaction(manager) - .addOptionValue(variant.id, option.id, "Default Value") + await productVariantServiceTx.addOptionValue( + variant.id, + option.id, + "Default Value" + ) } const result = await this.retrieve(productId) diff --git a/packages/medusa/src/services/return.js b/packages/medusa/src/services/return.js index 5de7d227a8..24ca142212 100644 --- a/packages/medusa/src/services/return.js +++ b/packages/medusa/src/services/return.js @@ -625,22 +625,23 @@ class ReturnService extends BaseService { const result = await returnRepository.save(updateObj) + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) for (const i of returnObj.items) { - const lineItem = await this.lineItemService_ - .withTransaction(manager) - .retrieve(i.item_id) + const lineItem = await lineItemServiceTx.retrieve(i.item_id) const returnedQuantity = (lineItem.returned_quantity || 0) + i.quantity - await this.lineItemService_.withTransaction(manager).update(i.item_id, { + await lineItemServiceTx.update(i.item_id, { returned_quantity: returnedQuantity, }) } + const inventoryServiceTx = this.inventoryService_.withTransaction(manager) for (const line of newLines) { const orderItem = order.items.find((i) => i.id === line.item_id) if (orderItem) { - await this.inventoryService_ - .withTransaction(manager) - .adjustInventory(orderItem.variant_id, line.received_quantity) + await inventoryServiceTx.adjustInventory( + orderItem.variant_id, + line.received_quantity + ) } } diff --git a/packages/medusa/src/services/shipping-profile.ts b/packages/medusa/src/services/shipping-profile.ts index 5d4a8cb15c..4ee1b8e3ba 100644 --- a/packages/medusa/src/services/shipping-profile.ts +++ b/packages/medusa/src/services/shipping-profile.ts @@ -297,20 +297,21 @@ class ShippingProfileService extends TransactionBaseService si.item_id === i.id) if (shipped) { const shippedQty = (i.shipped_quantity || 0) + shipped.quantity - await this.lineItemService_.withTransaction(manager).update(i.id, { + await lineItemServiceTx.update(i.id, { shipped_quantity: shippedQty, }) From 79acc38a57f92673ca1fc4cd4d933e874558e441 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Fri, 12 Aug 2022 11:17:39 +0200 Subject: [PATCH 15/34] feat(medusa): Simplify the transaction base service (#2007) **What** Simplify the transaction base service. **How** In fact, it does not need to be template and reduce the extensibility as the type is internally enforce. Now, the type is deduced by this which can be any derived class. --- .../routes/admin/uploads/get-download-url.ts | 2 +- .../__tests__/transaction-base-service.spec.ts | 2 +- .../medusa/src/interfaces/batch-job-strategy.ts | 14 +++++--------- packages/medusa/src/interfaces/file-service.ts | 9 ++++----- .../src/interfaces/notification-service.ts | 11 ++++------- .../medusa/src/interfaces/payment-service.ts | 10 ++++------ .../medusa/src/interfaces/search-service.ts | 10 ++++------ .../src/interfaces/transaction-base-service.ts | 17 +++++++---------- packages/medusa/src/loaders/search-index.ts | 2 +- packages/medusa/src/services/auth.ts | 2 +- packages/medusa/src/services/batch-job.ts | 2 +- packages/medusa/src/services/cart.ts | 2 +- packages/medusa/src/services/claim-item.ts | 2 +- packages/medusa/src/services/claim.ts | 5 +---- .../src/services/custom-shipping-option.ts | 2 +- packages/medusa/src/services/customer.ts | 2 +- .../medusa/src/services/discount-condition.ts | 2 +- packages/medusa/src/services/discount.ts | 2 +- packages/medusa/src/services/draft-order.ts | 2 +- packages/medusa/src/services/file.ts | 2 +- packages/medusa/src/services/fulfillment.ts | 2 +- packages/medusa/src/services/gift-card.ts | 2 +- packages/medusa/src/services/idempotency-key.ts | 2 +- packages/medusa/src/services/inventory.ts | 2 +- packages/medusa/src/services/note.ts | 2 +- packages/medusa/src/services/notification.ts | 6 +++--- packages/medusa/src/services/oauth.ts | 2 +- packages/medusa/src/services/order.ts | 2 +- .../medusa/src/services/payment-provider.ts | 2 +- packages/medusa/src/services/price-list.ts | 2 +- packages/medusa/src/services/pricing.ts | 2 +- .../medusa/src/services/product-collection.ts | 4 ++-- packages/medusa/src/services/product.ts | 5 +---- packages/medusa/src/services/return-reason.ts | 2 +- packages/medusa/src/services/sales-channel.ts | 2 +- packages/medusa/src/services/search.ts | 2 +- packages/medusa/src/services/shipping-option.ts | 2 +- .../medusa/src/services/shipping-profile.ts | 2 +- packages/medusa/src/services/store.ts | 2 +- .../medusa/src/services/strategy-resolver.ts | 17 +++++------------ packages/medusa/src/services/tax-provider.ts | 2 +- packages/medusa/src/services/totals.ts | 2 +- packages/medusa/src/services/user.ts | 6 ++---- .../src/strategies/batch-jobs/order/export.ts | 8 ++++---- .../src/strategies/batch-jobs/product/export.ts | 9 +++------ .../medusa/src/subscribers/search-indexing.ts | 4 ++-- 46 files changed, 82 insertions(+), 115 deletions(-) diff --git a/packages/medusa/src/api/routes/admin/uploads/get-download-url.ts b/packages/medusa/src/api/routes/admin/uploads/get-download-url.ts index dc354361c3..db264e85b1 100644 --- a/packages/medusa/src/api/routes/admin/uploads/get-download-url.ts +++ b/packages/medusa/src/api/routes/admin/uploads/get-download-url.ts @@ -31,7 +31,7 @@ import { IsString } from "class-validator" * description: The Download URL of the file */ export default async (req, res) => { - const fileService: AbstractFileService = req.scope.resolve("fileService") + const fileService: AbstractFileService = req.scope.resolve("fileService") const url = await fileService.getPresignedDownloadUrl({ fileKey: (req.validatedBody as AdminPostUploadsDownloadUrlReq).file_key, diff --git a/packages/medusa/src/interfaces/__tests__/transaction-base-service.spec.ts b/packages/medusa/src/interfaces/__tests__/transaction-base-service.spec.ts index 65c5b62608..44815a73fa 100644 --- a/packages/medusa/src/interfaces/__tests__/transaction-base-service.spec.ts +++ b/packages/medusa/src/interfaces/__tests__/transaction-base-service.spec.ts @@ -4,7 +4,7 @@ import { TransactionBaseService } from "../transaction-base-service" describe("TransactionBaseService", () => { it("should cloned the child class withTransaction", () => { - class Child extends TransactionBaseService { + class Child extends TransactionBaseService { protected manager_!: EntityManager protected transactionManager_!: EntityManager diff --git a/packages/medusa/src/interfaces/batch-job-strategy.ts b/packages/medusa/src/interfaces/batch-job-strategy.ts index a7f733e9dd..02609dd00a 100644 --- a/packages/medusa/src/interfaces/batch-job-strategy.ts +++ b/packages/medusa/src/interfaces/batch-job-strategy.ts @@ -4,8 +4,7 @@ import { ProductExportBatchJob } from "../strategies/batch-jobs/product" import { BatchJobService } from "../services" import { BatchJob } from "../models" -export interface IBatchJobStrategy> - extends TransactionBaseService { +export interface IBatchJobStrategy extends TransactionBaseService { /** * Method for preparing a batch job for processing */ @@ -30,12 +29,9 @@ export interface IBatchJobStrategy> buildTemplate(): Promise } -export abstract class AbstractBatchJobStrategy< - T extends TransactionBaseService, - TContainer = unknown - > - extends TransactionBaseService - implements IBatchJobStrategy +export abstract class AbstractBatchJobStrategy + extends TransactionBaseService + implements IBatchJobStrategy { static identifier: string static batchType: string @@ -113,6 +109,6 @@ export abstract class AbstractBatchJobStrategy< export function isBatchJobStrategy( object: unknown -): object is IBatchJobStrategy { +): object is IBatchJobStrategy { return object instanceof AbstractBatchJobStrategy } diff --git a/packages/medusa/src/interfaces/file-service.ts b/packages/medusa/src/interfaces/file-service.ts index e64a765fcb..11ac50668f 100644 --- a/packages/medusa/src/interfaces/file-service.ts +++ b/packages/medusa/src/interfaces/file-service.ts @@ -30,8 +30,7 @@ export type UploadStreamDescriptorType = { [x: string]: unknown } -export interface IFileService> - extends TransactionBaseService { +export interface IFileService extends TransactionBaseService { /** * upload file to fileservice * @param file Multer file from express multipart/form-data @@ -69,9 +68,9 @@ export interface IFileService> * */ getPresignedDownloadUrl(fileData: GetUploadedFileType): Promise } -export abstract class AbstractFileService> - extends TransactionBaseService - implements IFileService +export abstract class AbstractFileService + extends TransactionBaseService + implements IFileService { abstract upload( fileData: Express.Multer.File diff --git a/packages/medusa/src/interfaces/notification-service.ts b/packages/medusa/src/interfaces/notification-service.ts index a90a054b73..6b9d9ba8ff 100644 --- a/packages/medusa/src/interfaces/notification-service.ts +++ b/packages/medusa/src/interfaces/notification-service.ts @@ -7,8 +7,7 @@ type ReturnedData = { data: Record } -export interface INotificationService> - extends TransactionBaseService { +export interface INotificationService extends TransactionBaseService { sendNotification( event: string, data: unknown, @@ -22,11 +21,9 @@ export interface INotificationService> ): Promise } -export abstract class AbstractNotificationService< - T extends TransactionBaseService - > - extends TransactionBaseService - implements INotificationService +export abstract class AbstractNotificationService + extends TransactionBaseService + implements INotificationService { static identifier: string diff --git a/packages/medusa/src/interfaces/payment-service.ts b/packages/medusa/src/interfaces/payment-service.ts index 03a068f675..216052d230 100644 --- a/packages/medusa/src/interfaces/payment-service.ts +++ b/packages/medusa/src/interfaces/payment-service.ts @@ -12,8 +12,8 @@ export type Data = Record export type PaymentData = Data export type PaymentSessionData = Data -export interface PaymentService> - extends TransactionBaseService { +export interface PaymentService + extends TransactionBaseService { getIdentifier(): string getPaymentData(paymentSession: PaymentSession): Promise @@ -50,10 +50,8 @@ export interface PaymentService> getStatus(data: Data): Promise } -export abstract class AbstractPaymentService< - T extends TransactionBaseService - > - extends TransactionBaseService +export abstract class AbstractPaymentService + extends TransactionBaseService implements PaymentService { protected constructor(container: unknown, config?: Record) { diff --git a/packages/medusa/src/interfaces/search-service.ts b/packages/medusa/src/interfaces/search-service.ts index ac7563cc59..a481e45e4b 100644 --- a/packages/medusa/src/interfaces/search-service.ts +++ b/packages/medusa/src/interfaces/search-service.ts @@ -1,7 +1,7 @@ import { TransactionBaseService } from "./transaction-base-service" import { SearchService } from "medusa-interfaces" -export interface ISearchService> { +export interface ISearchService { options: Record /** @@ -72,11 +72,9 @@ export interface ISearchService> { updateSettings(indexName: string, settings: unknown): unknown } -export abstract class AbstractSearchService< - T extends TransactionBaseService - > - extends TransactionBaseService - implements ISearchService +export abstract class AbstractSearchService + extends TransactionBaseService + implements ISearchService { abstract readonly isDefault protected readonly options_: Record diff --git a/packages/medusa/src/interfaces/transaction-base-service.ts b/packages/medusa/src/interfaces/transaction-base-service.ts index 96d472bb23..ae9da955a9 100644 --- a/packages/medusa/src/interfaces/transaction-base-service.ts +++ b/packages/medusa/src/interfaces/transaction-base-service.ts @@ -1,34 +1,31 @@ import { EntityManager } from "typeorm" import { IsolationLevel } from "typeorm/driver/types/IsolationLevel" -export abstract class TransactionBaseService< - TChild extends TransactionBaseService, - TContainer = unknown -> { +export abstract class TransactionBaseService { protected abstract manager_: EntityManager protected abstract transactionManager_: EntityManager | undefined protected constructor( - protected readonly container: TContainer, - protected readonly configModule?: Record + protected readonly __container__: any, + protected readonly __configModule__?: Record ) {} - withTransaction(transactionManager?: EntityManager): this | TChild { + withTransaction(transactionManager?: EntityManager): this { if (!transactionManager) { return this } const cloned = new (this.constructor)( { - ...this.container, + ...this.__container__, manager: transactionManager, }, - this.configModule + this.__configModule__ ) cloned.transactionManager_ = transactionManager - return cloned as TChild + return cloned } protected shouldRetryTransaction_( diff --git a/packages/medusa/src/loaders/search-index.ts b/packages/medusa/src/loaders/search-index.ts index a3bd002cf4..1265ab00f7 100644 --- a/packages/medusa/src/loaders/search-index.ts +++ b/packages/medusa/src/loaders/search-index.ts @@ -22,7 +22,7 @@ export default async ({ container: MedusaContainer }): Promise => { const searchService = - container.resolve>("searchService") + container.resolve("searchService") const logger = container.resolve("logger") if (searchService.isDefault) { logger.warn( diff --git a/packages/medusa/src/services/auth.ts b/packages/medusa/src/services/auth.ts index 492ef5d18a..6393ba76b5 100644 --- a/packages/medusa/src/services/auth.ts +++ b/packages/medusa/src/services/auth.ts @@ -16,7 +16,7 @@ type InjectedDependencies = { * Can authenticate a user based on email password combination * @extends BaseService */ -class AuthService extends TransactionBaseService { +class AuthService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined protected readonly userService_: UserService diff --git a/packages/medusa/src/services/batch-job.ts b/packages/medusa/src/services/batch-job.ts index 57163b531f..5713686956 100644 --- a/packages/medusa/src/services/batch-job.ts +++ b/packages/medusa/src/services/batch-job.ts @@ -23,7 +23,7 @@ type InjectedDependencies = { strategyResolverService: StrategyResolverService } -class BatchJobService extends TransactionBaseService { +class BatchJobService extends TransactionBaseService { static readonly Events = { CREATED: "batch.created", UPDATED: "batch.updated", diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index ed5e142631..22297dd084 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -83,7 +83,7 @@ type TotalsConfig = { /* Provides layer to manipulate carts. * @implements BaseService */ -class CartService extends TransactionBaseService { +class CartService extends TransactionBaseService { static readonly Events = { CUSTOMER_UPDATED: "cart.customer_updated", CREATED: "cart.created", diff --git a/packages/medusa/src/services/claim-item.ts b/packages/medusa/src/services/claim-item.ts index 2129368c91..5a8681a6b8 100644 --- a/packages/medusa/src/services/claim-item.ts +++ b/packages/medusa/src/services/claim-item.ts @@ -11,7 +11,7 @@ import { buildQuery, setMetadata } from "../utils" import EventBusService from "./event-bus" import LineItemService from "./line-item" -class ClaimItemService extends BaseService { +class ClaimItemService extends BaseService { static Events = { CREATED: "claim_item.created", UPDATED: "claim_item.updated", diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index 1253d8acb9..b911896f98 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -49,10 +49,7 @@ type InjectedDependencies = { totalsService: TotalsService } -export default class ClaimService extends TransactionBaseService< - ClaimService, - InjectedDependencies -> { +export default class ClaimService extends TransactionBaseService { static readonly Events = { CREATED: "claim.created", UPDATED: "claim.updated", diff --git a/packages/medusa/src/services/custom-shipping-option.ts b/packages/medusa/src/services/custom-shipping-option.ts index 370d5d56f9..29f7b7860a 100644 --- a/packages/medusa/src/services/custom-shipping-option.ts +++ b/packages/medusa/src/services/custom-shipping-option.ts @@ -11,7 +11,7 @@ type InjectedDependencies = { manager: EntityManager customShippingOptionRepository: typeof CustomShippingOptionRepository } -class CustomShippingOptionService extends TransactionBaseService { +class CustomShippingOptionService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined protected customShippingOptionRepository_: typeof CustomShippingOptionRepository diff --git a/packages/medusa/src/services/customer.ts b/packages/medusa/src/services/customer.ts index 8487bfc9a0..fc5c1b6b7d 100644 --- a/packages/medusa/src/services/customer.ts +++ b/packages/medusa/src/services/customer.ts @@ -22,7 +22,7 @@ type InjectedDependencies = { /** * Provides layer to manipulate customers. */ -class CustomerService extends TransactionBaseService { +class CustomerService extends TransactionBaseService { protected readonly customerRepository_: typeof CustomerRepository protected readonly addressRepository_: typeof AddressRepository protected readonly eventBusService_: EventBusService diff --git a/packages/medusa/src/services/discount-condition.ts b/packages/medusa/src/services/discount-condition.ts index 560cdc0fad..fffbe8a4fe 100644 --- a/packages/medusa/src/services/discount-condition.ts +++ b/packages/medusa/src/services/discount-condition.ts @@ -27,7 +27,7 @@ type InjectedDependencies = { * Provides layer to manipulate discount conditions. * @implements {BaseService} */ -class DiscountConditionService extends TransactionBaseService { +class DiscountConditionService extends TransactionBaseService { protected readonly discountConditionRepository_: typeof DiscountConditionRepository protected readonly eventBus_: EventBusService diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index 5ea3d45427..7a16487c10 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -44,7 +44,7 @@ import { buildQuery, setMetadata } from "../utils" * Provides layer to manipulate discounts. * @implements {BaseService} */ -class DiscountService extends TransactionBaseService { +class DiscountService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined diff --git a/packages/medusa/src/services/draft-order.ts b/packages/medusa/src/services/draft-order.ts index e519aab001..5f237fa074 100644 --- a/packages/medusa/src/services/draft-order.ts +++ b/packages/medusa/src/services/draft-order.ts @@ -30,7 +30,7 @@ type InjectedDependencies = { * Handles draft orders * @implements {BaseService} */ -class DraftOrderService extends TransactionBaseService { +class DraftOrderService extends TransactionBaseService { static readonly Events = { CREATED: "draft_order.created", UPDATED: "draft_order.updated", diff --git a/packages/medusa/src/services/file.ts b/packages/medusa/src/services/file.ts index c8618d4cae..28f7309e23 100644 --- a/packages/medusa/src/services/file.ts +++ b/packages/medusa/src/services/file.ts @@ -8,7 +8,7 @@ import { UploadStreamDescriptorType, } from "../interfaces" -class DefaultFileService extends AbstractFileService { +class DefaultFileService extends AbstractFileService { upload(fileData: Express.Multer.File): Promise { throw new MedusaError( MedusaError.Types.UNEXPECTED_STATE, diff --git a/packages/medusa/src/services/fulfillment.ts b/packages/medusa/src/services/fulfillment.ts index 89a97b770d..ea882f94da 100644 --- a/packages/medusa/src/services/fulfillment.ts +++ b/packages/medusa/src/services/fulfillment.ts @@ -32,7 +32,7 @@ type InjectedDependencies = { /** * Handles Fulfillments */ -class FulfillmentService extends TransactionBaseService { +class FulfillmentService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined diff --git a/packages/medusa/src/services/gift-card.ts b/packages/medusa/src/services/gift-card.ts index 3da6c288e1..d2c626bf59 100644 --- a/packages/medusa/src/services/gift-card.ts +++ b/packages/medusa/src/services/gift-card.ts @@ -30,7 +30,7 @@ type InjectedDependencies = { /** * Provides layer to manipulate gift cards. */ -class GiftCardService extends TransactionBaseService { +class GiftCardService extends TransactionBaseService { protected readonly giftCardRepository_: typeof GiftCardRepository protected readonly giftCardTransactionRepo_: typeof GiftCardTransactionRepository protected readonly regionService_: RegionService diff --git a/packages/medusa/src/services/idempotency-key.ts b/packages/medusa/src/services/idempotency-key.ts index 85026a1055..12e4b8c37a 100644 --- a/packages/medusa/src/services/idempotency-key.ts +++ b/packages/medusa/src/services/idempotency-key.ts @@ -13,7 +13,7 @@ type InjectedDependencies = { idempotencyKeyRepository: typeof IdempotencyKeyRepository } -class IdempotencyKeyService extends TransactionBaseService { +class IdempotencyKeyService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined diff --git a/packages/medusa/src/services/inventory.ts b/packages/medusa/src/services/inventory.ts index e3d2c13d09..655de69c0a 100644 --- a/packages/medusa/src/services/inventory.ts +++ b/packages/medusa/src/services/inventory.ts @@ -9,7 +9,7 @@ type InventoryServiceProps = { manager: EntityManager productVariantService: ProductVariantService } -class InventoryService extends TransactionBaseService { +class InventoryService extends TransactionBaseService { protected readonly productVariantService_: ProductVariantService protected manager_: EntityManager diff --git a/packages/medusa/src/services/note.ts b/packages/medusa/src/services/note.ts index d2cc5519ec..fffc0f19c6 100644 --- a/packages/medusa/src/services/note.ts +++ b/packages/medusa/src/services/note.ts @@ -14,7 +14,7 @@ type InjectedDependencies = { eventBusService: EventBusService } -class NoteService extends TransactionBaseService { +class NoteService extends TransactionBaseService { static readonly Events = { CREATED: "note.created", UPDATED: "note.updated", diff --git a/packages/medusa/src/services/notification.ts b/packages/medusa/src/services/notification.ts index d67de09324..5c5fbcac97 100644 --- a/packages/medusa/src/services/notification.ts +++ b/packages/medusa/src/services/notification.ts @@ -19,14 +19,14 @@ type InjectedDependencies = { } type NotificationProviderKey = `noti_${string}` -class NotificationService extends TransactionBaseService { +class NotificationService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined protected subscribers_ = {} protected attachmentGenerator_: unknown = null protected readonly container_: InjectedDependencies & { - [key in `${NotificationProviderKey}`]: AbstractNotificationService + [key in `${NotificationProviderKey}`]: AbstractNotificationService } protected readonly logger_: Logger protected readonly notificationRepository_: typeof NotificationRepository @@ -151,7 +151,7 @@ class NotificationService extends TransactionBaseService { * @param id - the id of the provider * @return the notification provider */ - protected retrieveProvider_(id: string): AbstractNotificationService { + protected retrieveProvider_(id: string): AbstractNotificationService { try { return this.container_[`noti_${id}`] } catch (err) { diff --git a/packages/medusa/src/services/oauth.ts b/packages/medusa/src/services/oauth.ts index 8a819c7305..5ca811aaab 100644 --- a/packages/medusa/src/services/oauth.ts +++ b/packages/medusa/src/services/oauth.ts @@ -15,7 +15,7 @@ type InjectedDependencies = MedusaContainer & { oauthRepository: typeof OauthRepository } -class Oauth extends TransactionBaseService { +class Oauth extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined static Events = { diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 93115a1b9a..51f8be56e2 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -64,7 +64,7 @@ type InjectedDependencies = { eventBusService: EventBusService } -class OrderService extends TransactionBaseService { +class OrderService extends TransactionBaseService { static readonly Events = { GIFT_CARD_CREATED: "order.gift_card_created", PAYMENT_CAPTURED: "order.payment_captured", diff --git a/packages/medusa/src/services/payment-provider.ts b/packages/medusa/src/services/payment-provider.ts index b3f5de7cb3..075e1f748b 100644 --- a/packages/medusa/src/services/payment-provider.ts +++ b/packages/medusa/src/services/payment-provider.ts @@ -33,7 +33,7 @@ type InjectedDependencies = { /** * Helps retrieve payment providers */ -export default class PaymentProviderService extends TransactionBaseService { +export default class PaymentProviderService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined protected readonly container_: InjectedDependencies diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index f005c0c1fb..c15b45b30e 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -40,7 +40,7 @@ type PriceListConstructorProps = { * Provides layer to manipulate product tags. * @extends BaseService */ -class PriceListService extends TransactionBaseService { +class PriceListService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index 5cc9a50746..5840bb55d3 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -29,7 +29,7 @@ type InjectedDependencies = { * Allows retrieval of prices. * @extends BaseService */ -class PricingService extends TransactionBaseService { +class PricingService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined protected readonly regionService: RegionService diff --git a/packages/medusa/src/services/product-collection.ts b/packages/medusa/src/services/product-collection.ts index c95e17ef10..51a0422e61 100644 --- a/packages/medusa/src/services/product-collection.ts +++ b/packages/medusa/src/services/product-collection.ts @@ -7,7 +7,7 @@ import { ProductCollectionRepository } from "../repositories/product-collection" import { ExtendedFindConfig, FindConfig, QuerySelector } from "../types/common" import { CreateProductCollection, - UpdateProductCollection + UpdateProductCollection, } from "../types/product-collection" import { buildQuery, setMetadata } from "../utils" import { formatException } from "../utils/exception-formatter" @@ -23,7 +23,7 @@ type InjectedDependencies = { /** * Provides layer to manipulate product collections. */ -class ProductCollectionService extends TransactionBaseService { +class ProductCollectionService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 7346a4579d..a5bf4163fc 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -47,10 +47,7 @@ type InjectedDependencies = { featureFlagRouter: FlagRouter } -class ProductService extends TransactionBaseService< - ProductService, - InjectedDependencies -> { +class ProductService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined diff --git a/packages/medusa/src/services/return-reason.ts b/packages/medusa/src/services/return-reason.ts index d59e460598..1e099899dc 100644 --- a/packages/medusa/src/services/return-reason.ts +++ b/packages/medusa/src/services/return-reason.ts @@ -12,7 +12,7 @@ type InjectedDependencies = { returnReasonRepository: typeof ReturnReasonRepository } -class ReturnReasonService extends TransactionBaseService { +class ReturnReasonService extends TransactionBaseService { protected readonly retReasonRepo_: typeof ReturnReasonRepository protected manager_: EntityManager diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index 90d32d104a..57489ec971 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -20,7 +20,7 @@ type InjectedDependencies = { storeService: StoreService } -class SalesChannelService extends TransactionBaseService { +class SalesChannelService extends TransactionBaseService { static Events = { UPDATED: "sales_channel.updated", CREATED: "sales_channel.created", diff --git a/packages/medusa/src/services/search.ts b/packages/medusa/src/services/search.ts index c6297e82c3..d718d72128 100644 --- a/packages/medusa/src/services/search.ts +++ b/packages/medusa/src/services/search.ts @@ -7,7 +7,7 @@ type InjectedDependencies = { manager: EntityManager } -export default class DefaultSearchService extends AbstractSearchService { +export default class DefaultSearchService extends AbstractSearchService { isDefault = true protected manager_: EntityManager diff --git a/packages/medusa/src/services/shipping-option.ts b/packages/medusa/src/services/shipping-option.ts index 0eef3b2ce5..a11a68ae94 100644 --- a/packages/medusa/src/services/shipping-option.ts +++ b/packages/medusa/src/services/shipping-option.ts @@ -26,7 +26,7 @@ import RegionService from "./region" /** * Provides layer to manipulate profiles. */ -class ShippingOptionService extends TransactionBaseService { +class ShippingOptionService extends TransactionBaseService { protected readonly providerService_: FulfillmentProviderService protected readonly regionService_: RegionService protected readonly requirementRepository_: typeof ShippingOptionRequirementRepository diff --git a/packages/medusa/src/services/shipping-profile.ts b/packages/medusa/src/services/shipping-profile.ts index 4ee1b8e3ba..2eb599207f 100644 --- a/packages/medusa/src/services/shipping-profile.ts +++ b/packages/medusa/src/services/shipping-profile.ts @@ -32,7 +32,7 @@ type InjectedDependencies = { * @constructor * @implements {BaseService} */ -class ShippingProfileService extends TransactionBaseService { +class ShippingProfileService extends TransactionBaseService { protected readonly productService_: ProductService protected readonly shippingOptionService_: ShippingOptionService protected readonly customShippingOptionService_: CustomShippingOptionService diff --git a/packages/medusa/src/services/store.ts b/packages/medusa/src/services/store.ts index e2b9ece03e..348efb43ed 100644 --- a/packages/medusa/src/services/store.ts +++ b/packages/medusa/src/services/store.ts @@ -21,7 +21,7 @@ type InjectedDependencies = { * Provides layer to manipulate store settings. * @extends BaseService */ -class StoreService extends TransactionBaseService { +class StoreService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager diff --git a/packages/medusa/src/services/strategy-resolver.ts b/packages/medusa/src/services/strategy-resolver.ts index 8983a3246e..070181a412 100644 --- a/packages/medusa/src/services/strategy-resolver.ts +++ b/packages/medusa/src/services/strategy-resolver.ts @@ -7,26 +7,19 @@ type InjectedDependencies = { [key: string]: unknown } -export default class StrategyResolver extends TransactionBaseService< - StrategyResolver, - InjectedDependencies -> { +export default class StrategyResolver extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined - constructor(container: InjectedDependencies) { + constructor(protected readonly container: InjectedDependencies) { super(container) this.manager_ = container.manager } - resolveBatchJobByType>( - type: string - ): AbstractBatchJobStrategy { - let resolved: AbstractBatchJobStrategy + resolveBatchJobByType(type: string): AbstractBatchJobStrategy { + let resolved: AbstractBatchJobStrategy try { - resolved = this.container[ - `batchType_${type}` - ] as AbstractBatchJobStrategy + resolved = this.container[`batchType_${type}`] as AbstractBatchJobStrategy } catch (e) { throw new MedusaError( MedusaError.Types.NOT_FOUND, diff --git a/packages/medusa/src/services/tax-provider.ts b/packages/medusa/src/services/tax-provider.ts index 6020c4113e..13d7bd503b 100644 --- a/packages/medusa/src/services/tax-provider.ts +++ b/packages/medusa/src/services/tax-provider.ts @@ -38,7 +38,7 @@ type RegionDetails = { /** * Finds tax providers and assists in tax related operations. */ -class TaxProviderService extends TransactionBaseService { +class TaxProviderService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager diff --git a/packages/medusa/src/services/totals.ts b/packages/medusa/src/services/totals.ts index 8addac9e7a..5ff720d5e4 100644 --- a/packages/medusa/src/services/totals.ts +++ b/packages/medusa/src/services/totals.ts @@ -90,7 +90,7 @@ type CalculationContextOptions = { * A service that calculates total and subtotals for orders, carts etc.. * @implements {BaseService} */ -class TotalsService extends TransactionBaseService { +class TotalsService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager diff --git a/packages/medusa/src/services/user.ts b/packages/medusa/src/services/user.ts index 7c7a5f333d..da101ebb98 100644 --- a/packages/medusa/src/services/user.ts +++ b/packages/medusa/src/services/user.ts @@ -24,7 +24,7 @@ type UserServiceProps = { * Provides layer to manipulate users. * @extends BaseService */ -class UserService extends TransactionBaseService { +class UserService extends TransactionBaseService { static Events = { PASSWORD_RESET: "user.password_reset", CREATED: "user.created", @@ -51,9 +51,7 @@ class UserService extends TransactionBaseService { * @return {string} the validated email */ validateEmail_(email: string): string { - const schema = Validator.string() - .email() - .required() + const schema = Validator.string().email().required() const { value, error } = schema.validate(email) if (error) { throw new MedusaError( diff --git a/packages/medusa/src/strategies/batch-jobs/order/export.ts b/packages/medusa/src/strategies/batch-jobs/order/export.ts index 6653d23d61..3b41286fea 100644 --- a/packages/medusa/src/strategies/batch-jobs/order/export.ts +++ b/packages/medusa/src/strategies/batch-jobs/order/export.ts @@ -18,14 +18,14 @@ import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channe import { FindConfig } from "../../../types/common" type InjectedDependencies = { - fileService: IFileService + fileService: IFileService orderService: OrderService batchJobService: BatchJobService manager: EntityManager featureFlagRouter: FlagRouter } -class OrderExportStrategy extends AbstractBatchJobStrategy { +class OrderExportStrategy extends AbstractBatchJobStrategy { public static identifier = "order-export-strategy" public static batchType = "order-export" @@ -37,7 +37,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy protected manager_: EntityManager protected transactionManager_: EntityManager | undefined - protected readonly fileService_: IFileService + protected readonly fileService_: IFileService protected readonly batchJobService_: BatchJobService protected readonly orderService_: OrderService protected readonly featureFlagRouter_: FlagRouter @@ -258,7 +258,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy await this.fileService_ .withTransaction(transactionManager) - .delete({ key: fileKey }) + .delete({ fileKey: fileKey }) return } diff --git a/packages/medusa/src/strategies/batch-jobs/product/export.ts b/packages/medusa/src/strategies/batch-jobs/product/export.ts index 2e52e70c46..ef88c3ac13 100644 --- a/packages/medusa/src/strategies/batch-jobs/product/export.ts +++ b/packages/medusa/src/strategies/batch-jobs/product/export.ts @@ -20,14 +20,11 @@ type InjectedDependencies = { manager: EntityManager batchJobService: BatchJobService productService: ProductService - fileService: IFileService + fileService: IFileService featureFlagRouter: FlagRouter } -export default class ProductExportStrategy extends AbstractBatchJobStrategy< - ProductExportStrategy, - InjectedDependencies -> { +export default class ProductExportStrategy extends AbstractBatchJobStrategy { public static identifier = "product-export-strategy" public static batchType = "product-export" @@ -36,7 +33,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy< protected readonly batchJobService_: BatchJobService protected readonly productService_: ProductService - protected readonly fileService_: IFileService + protected readonly fileService_: IFileService protected readonly featureFlagRouter_: FlagRouter protected readonly defaultRelations_ = [ diff --git a/packages/medusa/src/subscribers/search-indexing.ts b/packages/medusa/src/subscribers/search-indexing.ts index 2f7f2f31f7..e9c060a20d 100644 --- a/packages/medusa/src/subscribers/search-indexing.ts +++ b/packages/medusa/src/subscribers/search-indexing.ts @@ -7,13 +7,13 @@ import { ISearchService } from "../interfaces" type InjectedDependencies = { eventBusService: EventBusService - searchService: ISearchService + searchService: ISearchService productService: ProductService } class SearchIndexingSubscriber { private readonly eventBusService_: EventBusService - private readonly searchService_: ISearchService + private readonly searchService_: ISearchService private readonly productService_: ProductService constructor({ From 15a5b029ae3bd954481c558beeac87ace7ab945d Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 15 Aug 2022 13:23:36 +0200 Subject: [PATCH 16/34] fix(medusa): join tracking links to all fulfillments in admin/orders (#2045) Fixes https://github.com/medusajs/medusa/issues/2042 --- .changeset/cyan-years-complain.md | 5 +++++ .../src/api/routes/admin/orders/__tests__/get-order.js | 2 ++ packages/medusa/src/api/routes/admin/orders/index.ts | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 .changeset/cyan-years-complain.md diff --git a/.changeset/cyan-years-complain.md b/.changeset/cyan-years-complain.md new file mode 100644 index 0000000000..321c100b16 --- /dev/null +++ b/.changeset/cyan-years-complain.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Join tracking links to all fulfillments in admin/orders diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js index 3c75a6a93f..a6d1810a8c 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js @@ -27,6 +27,7 @@ const defaultRelations = [ "claims.shipping_address", "claims.additional_items", "claims.fulfillments", + "claims.fulfillments.tracking_links", "claims.claim_items", "claims.claim_items.item", "claims.claim_items.images", @@ -37,6 +38,7 @@ const defaultRelations = [ "swaps.shipping_address", "swaps.additional_items", "swaps.fulfillments", + "swaps.fulfillments.tracking_links", ] const defaultFields = [ diff --git a/packages/medusa/src/api/routes/admin/orders/index.ts b/packages/medusa/src/api/routes/admin/orders/index.ts index 5da3245722..e2a34f2838 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.ts +++ b/packages/medusa/src/api/routes/admin/orders/index.ts @@ -258,6 +258,7 @@ export const defaultAdminOrdersRelations = [ "claims.shipping_address", "claims.additional_items", "claims.fulfillments", + "claims.fulfillments.tracking_links", "claims.claim_items", "claims.claim_items.item", "claims.claim_items.images", @@ -269,6 +270,7 @@ export const defaultAdminOrdersRelations = [ "swaps.shipping_address", "swaps.additional_items", "swaps.fulfillments", + "swaps.fulfillments.tracking_links", ] export const defaultAdminOrdersFields = [ From f1c2c6c68b373d40273da8dc2138acbb691433df Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 16 Aug 2022 10:47:23 +0200 Subject: [PATCH 17/34] feat(medusa): Implement the SC migration scripts (#2037) **What** Migrate the existing products to the default sales channel FIXES CORE-434 --- .../src/scripts/sales-channels-migration.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 packages/medusa/src/scripts/sales-channels-migration.ts diff --git a/packages/medusa/src/scripts/sales-channels-migration.ts b/packages/medusa/src/scripts/sales-channels-migration.ts new file mode 100644 index 0000000000..a317923667 --- /dev/null +++ b/packages/medusa/src/scripts/sales-channels-migration.ts @@ -0,0 +1,86 @@ +import dotenv from "dotenv" +import { createConnection } from "typeorm" +import Logger from "../loaders/logger" +import { Product } from "../models/product"; +import { Store } from "../models/store"; + +dotenv.config() + +const typeormConfig = { + type: process.env.TYPEORM_CONNECTION, + url: process.env.TYPEORM_URL, + username: process.env.TYPEORM_USERNAME, + password: process.env.TYPEORM_PASSWORD, + database: process.env.TYPEORM_DATABASE, + migrations: [process.env.TYPEORM_MIGRATIONS as string], + entities: [process.env.TYPEORM_ENTITIES], + logging: true, +} + +const migrate = async function ({ typeormConfig }): Promise { + const connection = await createConnection(typeormConfig) + + const BATCH_SIZE = 1000 + + await connection.transaction(async (manager) => { + const store = await manager + .createQueryBuilder() + .from(Store, "store") + .select("store.default_sales_channel_id") + .getOne() as Store + + if (!store.default_sales_channel_id) { + Logger.error(`The default sales channel does not exists yet. Run your project and then re run the migration.`) + process.exit(1) + return + } + + let shouldContinue = true + while (shouldContinue) { + const products = await manager + .createQueryBuilder() + .from(Product, "product") + .leftJoin("product_sales_channel", "product_sc", "product_sc.product_id = product.id") + .andWhere("product_sc.product_id IS NULL") + .select("product.id as id") + .distinct(true) + .limit(BATCH_SIZE) + .getRawMany() + + if (products.length > 0) { + await manager + .createQueryBuilder() + .insert() + .into("product_sales_channel") + .values( + products.map((product) => ({ + sales_channel_id: store.default_sales_channel_id, + product_id: product.id + })) + ) + .orIgnore() + .execute() + } + + const danglingProductCount = await manager + .createQueryBuilder() + .from(Product, "product") + .leftJoin("product_sales_channel", "product_sc", "product_sc.product_id = product.id") + .andWhere("product_sc.product_id IS NULL") + .getCount() + shouldContinue = !!danglingProductCount + } + }) + + Logger.info(`Product entities have been successfully migrated`) + process.exit() +} + +migrate({ typeormConfig }) + .then(() => { + Logger.info("Database migration completed successfully") + process.exit() + }) + .catch((err) => console.log(err)) + +export default migrate From 0ba63c70b03899cd4cc966b5bc4d0220c6ba2b45 Mon Sep 17 00:00:00 2001 From: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:21:01 +0200 Subject: [PATCH 18/34] fix(medusa): Complete cart with 100% discount (#2032) **What** Naive fix to allow carts with 100% discount to be completed. **Why** Discount total is wrongly calculated if `items` and `items.adjustments` is not included in relations upon retrieving the cart. **Thought** This is yet another example of why we need to rethink and refactor totals computation to not depend on what is provided by the user. --- integration-tests/api/__tests__/store/cart.js | 30 ++++++++++++ .../medusa/src/services/__tests__/order.js | 46 ++++++++++--------- packages/medusa/src/services/cart.ts | 18 ++++---- packages/medusa/src/services/order.ts | 44 +++++++++--------- .../medusa/src/strategies/cart-completion.ts | 7 ++- 5 files changed, 93 insertions(+), 52 deletions(-) diff --git a/integration-tests/api/__tests__/store/cart.js b/integration-tests/api/__tests__/store/cart.js index 87ea99a0bf..62aea76e1a 100644 --- a/integration-tests/api/__tests__/store/cart.js +++ b/integration-tests/api/__tests__/store/cart.js @@ -1635,6 +1635,36 @@ describe("/store/carts", () => { expect(getRes.data.type).toEqual("order") }) + it("complete cart with 100% discount", async () => { + await simpleDiscountFactory(dbConnection, { + code: "100PERCENT", + rule: { + type: "percentage", + value: 100, + }, + regions: ["test-region"], + }) + + const api = useApi() + + await api + .post(`/store/carts/test-cart-3`, { + discounts: [{ code: "100PERCENT" }], + }) + .catch((err) => { + console.log(err.response.data) + }) + + const getRes = await api + .post(`/store/carts/test-cart-3/complete`) + .catch((err) => { + console.log(err.response.data) + }) + + expect(getRes.status).toEqual(200) + expect(getRes.data.type).toEqual("order") + }) + it("complete cart with items inventory covered", async () => { const api = useApi() const getRes = await api.post(`/store/carts/test-cart-2/complete-cart`) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index 6198601231..83e8d837cd 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1,11 +1,11 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import OrderService from "../order" import { InventoryServiceMock } from "../__mocks__/inventory" -import { LineItemServiceMock } from "../__mocks__/line-item"; +import { LineItemServiceMock } from "../__mocks__/line-item" describe("OrderService", () => { const totalsService = { - withTransaction: function () { + withTransaction: function() { return this }, getLineItemRefund: () => {}, @@ -40,7 +40,7 @@ describe("OrderService", () => { const eventBusService = { emit: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -56,20 +56,20 @@ describe("OrderService", () => { }) const lineItemService = { update: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } const shippingOptionService = { updateShippingMethod: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } const giftCardService = { update: jest.fn(), createTransaction: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -81,7 +81,7 @@ describe("OrderService", () => { cancelPayment: jest.fn().mockImplementation((payment) => { return Promise.resolve({ ...payment, status: "cancelled" }) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -120,7 +120,7 @@ describe("OrderService", () => { total: 100, }) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -210,6 +210,8 @@ describe("OrderService", () => { "discounts.rule", "gift_cards", "shipping_methods", + "items", + "items.adjustments", ], }) @@ -521,7 +523,7 @@ describe("OrderService", () => { manager: MockManager, orderRepository: orderRepo, eventBusService, - lineItemService: LineItemServiceMock + lineItemService: LineItemServiceMock, }) beforeEach(async () => { @@ -615,14 +617,14 @@ describe("OrderService", () => { const fulfillmentService = { cancelFulfillment: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } const paymentProviderService = { cancelPayment: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -719,7 +721,7 @@ describe("OrderService", () => { ? Promise.reject() : Promise.resolve({ ...p, captured_at: "notnull" }) ), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -824,7 +826,7 @@ describe("OrderService", () => { const lineItemService = { update: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -837,7 +839,7 @@ describe("OrderService", () => { }, ]) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1004,7 +1006,7 @@ describe("OrderService", () => { }) } }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1073,7 +1075,7 @@ describe("OrderService", () => { .mockImplementation((p) => p.id === "payment_fail" ? Promise.reject() : Promise.resolve() ), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1214,7 +1216,7 @@ describe("OrderService", () => { .fn() .mockImplementation(() => Promise.resolve({})), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1348,7 +1350,7 @@ describe("OrderService", () => { const lineItemService = { update: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1371,7 +1373,7 @@ describe("OrderService", () => { ], }) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1398,7 +1400,9 @@ describe("OrderService", () => { ) expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1) - expect(fulfillmentService.createShipment).toHaveBeenCalledWith( + expect( + fulfillmentService.createShipment + ).toHaveBeenCalledWith( IdMap.getId("fulfillment"), [{ tracking_number: "1234" }, { tracking_number: "2345" }], { metadata: undefined, no_notification: true } @@ -1490,7 +1494,7 @@ describe("OrderService", () => { refundPayment: jest .fn() .mockImplementation((p) => Promise.resolve({ id: "ref" })), - withTransaction: function () { + withTransaction: function() { return this }, } diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 22297dd084..b7eb19395c 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -3,17 +3,18 @@ import { MedusaError, Validator } from "medusa-core-utils" import { DeepPartial, EntityManager, In } from "typeorm" import { TransactionBaseService } from "../interfaces" import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy" +import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { Address, Cart, - CustomShippingOption, Customer, + CustomShippingOption, Discount, - LineItem, - ShippingMethod, - SalesChannel, DiscountRuleType, + LineItem, PaymentSession, + SalesChannel, + ShippingMethod, } from "../models" import { AddressRepository } from "../repositories/address" import { CartRepository } from "../repositories/cart" @@ -28,11 +29,13 @@ import { } from "../types/cart" import { AddressPayload, FindConfig, TotalField } from "../types/common" import { buildQuery, isDefined, setMetadata, validateId } from "../utils" +import { FlagRouter } from "../utils/flag-router" import CustomShippingOptionService from "./custom-shipping-option" import CustomerService from "./customer" import DiscountService from "./discount" import EventBusService from "./event-bus" import GiftCardService from "./gift-card" +import { SalesChannelService } from "./index" import InventoryService from "./inventory" import LineItemService from "./line-item" import LineItemAdjustmentService from "./line-item-adjustment" @@ -41,12 +44,9 @@ import ProductService from "./product" import ProductVariantService from "./product-variant" import RegionService from "./region" import ShippingOptionService from "./shipping-option" +import StoreService from "./store" import TaxProviderService from "./tax-provider" import TotalsService from "./totals" -import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" -import { FlagRouter } from "../utils/flag-router" -import StoreService from "./store" -import { SalesChannelService } from "./index" type InjectedDependencies = { manager: EntityManager @@ -1373,7 +1373,7 @@ class CartService extends TransactionBaseService { // If cart total is 0, we don't perform anything payment related if (cart.total <= 0) { cart.payment_authorized_at = new Date() - return cartRepository.save(cart) + return await cartRepository.save(cart) } if (!cart.payment_session) { diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 51f8be56e2..ed2bc869b8 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -1,25 +1,6 @@ import { MedusaError } from "medusa-core-utils" import { Brackets, EntityManager } from "typeorm" -import CustomerService from "./customer" -import { OrderRepository } from "../repositories/order" -import PaymentProviderService from "./payment-provider" -import ShippingOptionService from "./shipping-option" -import ShippingProfileService from "./shipping-profile" -import DiscountService from "./discount" -import FulfillmentProviderService from "./fulfillment-provider" -import FulfillmentService from "./fulfillment" -import LineItemService from "./line-item" -import TotalsService from "./totals" -import RegionService from "./region" -import CartService from "./cart" -import { AddressRepository } from "../repositories/address" -import GiftCardService from "./gift-card" -import DraftOrderService from "./draft-order" -import InventoryService from "./inventory" -import EventBusService from "./event-bus" import { TransactionBaseService } from "../interfaces" -import { buildQuery, setMetadata } from "../utils" -import { FindConfig, QuerySelector, Selector } from "../types/common" import { Address, ClaimOrder, @@ -36,12 +17,31 @@ import { Swap, TrackingLink, } from "../models" -import { UpdateOrderInput } from "../types/orders" -import { CreateShippingMethodDto } from "../types/shipping-options" +import { AddressRepository } from "../repositories/address" +import { OrderRepository } from "../repositories/order" +import { FindConfig, QuerySelector, Selector } from "../types/common" import { CreateFulfillmentOrder, FulFillmentItemType, } from "../types/fulfillment" +import { UpdateOrderInput } from "../types/orders" +import { CreateShippingMethodDto } from "../types/shipping-options" +import { buildQuery, setMetadata } from "../utils" +import CartService from "./cart" +import CustomerService from "./customer" +import DiscountService from "./discount" +import DraftOrderService from "./draft-order" +import EventBusService from "./event-bus" +import FulfillmentService from "./fulfillment" +import FulfillmentProviderService from "./fulfillment-provider" +import GiftCardService from "./gift-card" +import InventoryService from "./inventory" +import LineItemService from "./line-item" +import PaymentProviderService from "./payment-provider" +import RegionService from "./region" +import ShippingOptionService from "./shipping-option" +import ShippingProfileService from "./shipping-profile" +import TotalsService from "./totals" type InjectedDependencies = { manager: EntityManager @@ -492,6 +492,8 @@ class OrderService extends TransactionBaseService { "discounts.rule", "gift_cards", "shipping_methods", + "items", + "items.adjustments", ], }) diff --git a/packages/medusa/src/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index 5f50255932..0ff7ea662f 100644 --- a/packages/medusa/src/strategies/cart-completion.ts +++ b/packages/medusa/src/strategies/cart-completion.ts @@ -158,7 +158,12 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { .withTransaction(manager) .retrieve(id, { select: ["total"], - relations: ["payment", "payment_sessions"], + relations: [ + "items", + "items.adjustments", + "payment", + "payment_sessions", + ], }) // If cart is part of swap, we register swap as complete From fb1105b9c0069cea1d5ee65dcde169cc5e0ea3a8 Mon Sep 17 00:00:00 2001 From: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:36:57 +0200 Subject: [PATCH 19/34] fix(medusa-payment-stripe): Add item adjustments relation in CartSubcriber (#2052) --- packages/medusa-payment-stripe/src/subscribers/cart.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/medusa-payment-stripe/src/subscribers/cart.js b/packages/medusa-payment-stripe/src/subscribers/cart.js index e4ccf80d13..e6a2fac15c 100644 --- a/packages/medusa-payment-stripe/src/subscribers/cart.js +++ b/packages/medusa-payment-stripe/src/subscribers/cart.js @@ -30,6 +30,8 @@ class CartSubscriber { "shipping_address", "region", "region.payment_providers", + "items", + "items.adjustments", "payment_sessions", "customer", ], @@ -44,7 +46,7 @@ class CartSubscriber { ) if (session) { - return this.paymentProviderService_.updateSession(session, cart) + return await this.paymentProviderService_.updateSession(session, cart) } } } From 9c3c8cb2f6ac046c7387d5bdb7646538696a25bf Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 17 Aug 2022 15:16:06 +0300 Subject: [PATCH 20/34] added upgrade guide for v1.3.6 --- .../advanced/backend/upgrade-guides/1-3-6.md | 39 +++++++++++++++++++ www/docs/sidebars.js | 5 +++ 2 files changed, 44 insertions(+) create mode 100644 docs/content/advanced/backend/upgrade-guides/1-3-6.md diff --git a/docs/content/advanced/backend/upgrade-guides/1-3-6.md b/docs/content/advanced/backend/upgrade-guides/1-3-6.md new file mode 100644 index 0000000000..240ba4cbc7 --- /dev/null +++ b/docs/content/advanced/backend/upgrade-guides/1-3-6.md @@ -0,0 +1,39 @@ +# v1.3.6 + +Following the addition of feature flags in version v1.3.3 and the addition of the Sales Channels API in v1.3.5, v1.3.6 introduces a data migration script that moves all products into the Default Sales Channel. + +:::note + +In version 1.3.6, Sales Channels are available but hidden behind feature flags. If you don’t have Sales Channels enabled, you don’t need to follow the steps detailed in this migration script. + +::: + +## Prerequisites + +Before performing the actions mentioned in this guide, you must set the following environment variables: + +```bash +TYPEORM_CONNECTION=postgres +TYPEORM_URL= +TYPEORM_LOGGING=true +TYPEORM_ENTITIES=./node_modules/@medusajs/medusa/dist/models/*.js +TYPEORM_MIGRATIONS=./node_modules/@medusajs/medusa/dist/migrations/*.js +``` + +These environment variables are used in the data migration scripts in this upgrade. Make sure to replace `` with your PostgreSQL database URL. + +## Sales Channels + +Sales Channels were introduced in v1.3.5 behind a feature flag. By enabling Sales Channels, developers and users can associate products and other entities with a specific Sales Channel. + +However, if you upgraded Medusa to v1.3.5 and enabled Sales Channels, you must add every product to at least one Sales Channel manually. Otherwise, products can’t be added to carts in different Sales Channels. + +v1.3.6 introduces a data migration script that automates this process for you by moving all your products into a default Sales Channel. This ensures that you can use the Sales Channels feature without it affecting the user experience in your store. + +### Actions Required + +If you’ve enabled Sales Channels, it’s essential to run the data migration script after upgrading your server and before starting your Medusa server: + +```bash +node ./node_modules/@medusajs/medusa/dist/scripts/sales-channels-migration.js +``` diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index 4c1941de5d..26576fe353 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -291,6 +291,11 @@ module.exports = { id: "advanced/backend/upgrade-guides/1-3-0", label: "v1.3.0" }, + { + type: "doc", + id: "advanced/backend/upgrade-guides/1-3-6", + label: "v1.3.6" + }, ] }, ] From d617e21bf582610ffeca851454b0c153031276bc Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 17 Aug 2022 15:17:33 +0300 Subject: [PATCH 21/34] Revert "added upgrade guide for v1.3.6" This reverts commit 9c3c8cb2f6ac046c7387d5bdb7646538696a25bf. --- .../advanced/backend/upgrade-guides/1-3-6.md | 39 ------------------- www/docs/sidebars.js | 5 --- 2 files changed, 44 deletions(-) delete mode 100644 docs/content/advanced/backend/upgrade-guides/1-3-6.md diff --git a/docs/content/advanced/backend/upgrade-guides/1-3-6.md b/docs/content/advanced/backend/upgrade-guides/1-3-6.md deleted file mode 100644 index 240ba4cbc7..0000000000 --- a/docs/content/advanced/backend/upgrade-guides/1-3-6.md +++ /dev/null @@ -1,39 +0,0 @@ -# v1.3.6 - -Following the addition of feature flags in version v1.3.3 and the addition of the Sales Channels API in v1.3.5, v1.3.6 introduces a data migration script that moves all products into the Default Sales Channel. - -:::note - -In version 1.3.6, Sales Channels are available but hidden behind feature flags. If you don’t have Sales Channels enabled, you don’t need to follow the steps detailed in this migration script. - -::: - -## Prerequisites - -Before performing the actions mentioned in this guide, you must set the following environment variables: - -```bash -TYPEORM_CONNECTION=postgres -TYPEORM_URL= -TYPEORM_LOGGING=true -TYPEORM_ENTITIES=./node_modules/@medusajs/medusa/dist/models/*.js -TYPEORM_MIGRATIONS=./node_modules/@medusajs/medusa/dist/migrations/*.js -``` - -These environment variables are used in the data migration scripts in this upgrade. Make sure to replace `` with your PostgreSQL database URL. - -## Sales Channels - -Sales Channels were introduced in v1.3.5 behind a feature flag. By enabling Sales Channels, developers and users can associate products and other entities with a specific Sales Channel. - -However, if you upgraded Medusa to v1.3.5 and enabled Sales Channels, you must add every product to at least one Sales Channel manually. Otherwise, products can’t be added to carts in different Sales Channels. - -v1.3.6 introduces a data migration script that automates this process for you by moving all your products into a default Sales Channel. This ensures that you can use the Sales Channels feature without it affecting the user experience in your store. - -### Actions Required - -If you’ve enabled Sales Channels, it’s essential to run the data migration script after upgrading your server and before starting your Medusa server: - -```bash -node ./node_modules/@medusajs/medusa/dist/scripts/sales-channels-migration.js -``` diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index 26576fe353..4c1941de5d 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -291,11 +291,6 @@ module.exports = { id: "advanced/backend/upgrade-guides/1-3-0", label: "v1.3.0" }, - { - type: "doc", - id: "advanced/backend/upgrade-guides/1-3-6", - label: "v1.3.6" - }, ] }, ] From 844d7d1f5f42a850ced5e70431b7590eafa390ac Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Fri, 19 Aug 2022 17:26:12 +0200 Subject: [PATCH 22/34] feat(medusa): Migrate Return service to ts (#1926) --- .../api/routes/admin/orders/request-return.ts | 35 +- .../api/routes/admin/returns/list-returns.ts | 4 +- .../api/routes/store/returns/create-return.ts | 138 +++---- packages/medusa/src/services/claim.ts | 14 +- packages/medusa/src/services/line-item.ts | 7 +- .../src/services/{return.js => return.ts} | 373 ++++++++++-------- packages/medusa/src/types/return.ts | 30 ++ 7 files changed, 338 insertions(+), 263 deletions(-) rename packages/medusa/src/services/{return.js => return.ts} (62%) create mode 100644 packages/medusa/src/types/return.ts diff --git a/packages/medusa/src/api/routes/admin/orders/request-return.ts b/packages/medusa/src/api/routes/admin/orders/request-return.ts index afae0127d6..4fa67a96fc 100644 --- a/packages/medusa/src/api/routes/admin/orders/request-return.ts +++ b/packages/medusa/src/api/routes/admin/orders/request-return.ts @@ -1,8 +1,3 @@ -import { - EventBusService, - OrderService, - ReturnService, -} from "../../../../services" import { IsArray, IsBoolean, @@ -12,13 +7,19 @@ import { ValidateNested, } from "class-validator" import { defaultAdminOrdersFields, defaultAdminOrdersRelations } from "." +import { + EventBusService, + OrderService, + ReturnService, +} from "../../../../services" -import { MedusaError } from "medusa-core-utils" -import { OrdersReturnItem } from "../../../../types/orders" import { Type } from "class-transformer" -import { validator } from "../../../../utils/validator" +import { MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" +import { Order, Return } from "../../../../models" +import { OrdersReturnItem } from "../../../../types/orders" import { isDefined } from "../../../../utils" +import { validator } from "../../../../utils/validator" /** * @oas [post] /orders/{id}/return @@ -197,7 +198,7 @@ export default async (req, res) => { const { key, error } = await idempotencyKeyService .withTransaction(transactionManager) .workStage(idempotencyKey.idempotency_key, async (manager) => { - let order = await orderService + let order: Order | Return = await orderService .withTransaction(manager) .retrieve(id, { relations: ["returns"] }) @@ -206,22 +207,24 @@ export default async (req, res) => { * and register it as received. */ if (value.receive_now) { - let ret = await returnService.withTransaction(manager).list({ - idempotency_key: idempotencyKey.idempotency_key, - }) + const returns = await returnService + .withTransaction(manager) + .list({ + idempotency_key: idempotencyKey.idempotency_key, + }) - if (!ret.length) { + if (!returns.length) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `Return not found` ) } - ret = ret[0] + const returnOrder = returns[0] order = await returnService .withTransaction(manager) - .receive(ret.id, value.items, value.refund) + .receive(returnOrder.id, value.items, value.refund) } order = await orderService @@ -278,7 +281,7 @@ export default async (req, res) => { } type ReturnObj = { - order_id?: string + order_id: string idempotency_key?: string items?: OrdersReturnItem[] shipping_method?: ReturnShipping diff --git a/packages/medusa/src/api/routes/admin/returns/list-returns.ts b/packages/medusa/src/api/routes/admin/returns/list-returns.ts index bd2f709b40..3f0d3124ab 100644 --- a/packages/medusa/src/api/routes/admin/returns/list-returns.ts +++ b/packages/medusa/src/api/routes/admin/returns/list-returns.ts @@ -3,6 +3,8 @@ import { IsNumber, IsOptional } from "class-validator" import { ReturnService } from "../../../../services" import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" +import { FindConfig } from "../../../../types/common" +import { Return } from "../../../../models" /** * @oas [get] /returns @@ -47,7 +49,7 @@ export default async (req, res) => { skip: validated.offset, take: validated.limit, order: { created_at: "DESC" }, - } + } as FindConfig const returns = await returnService.list(selector, { ...listConfig }) diff --git a/packages/medusa/src/api/routes/store/returns/create-return.ts b/packages/medusa/src/api/routes/store/returns/create-return.ts index 50229128d9..e93538dcfc 100644 --- a/packages/medusa/src/api/routes/store/returns/create-return.ts +++ b/packages/medusa/src/api/routes/store/returns/create-return.ts @@ -8,14 +8,14 @@ import { ValidateNested, } from "class-validator" +import { Type } from "class-transformer" +import { MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" import EventBusService from "../../../../services/event-bus" import IdempotencyKeyService from "../../../../services/idempotency-key" -import { MedusaError } from "medusa-core-utils" import OrderService from "../../../../services/order" import ReturnService from "../../../../services/return" -import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" -import { EntityManager } from "typeorm"; /** * @oas [post] /returns @@ -87,12 +87,9 @@ export default async (req, res) => { let idempotencyKey try { await manager.transaction(async (transactionManager) => { - idempotencyKey = await idempotencyKeyService.withTransaction(transactionManager).initializeRequest( - headerKey, - req.method, - req.params, - req.path - ) + idempotencyKey = await idempotencyKeyService + .withTransaction(transactionManager) + .initializeRequest(headerKey, req.method, req.params, req.path) }) } catch (error) { res.status(409).send("Failed to create idempotency key") @@ -116,48 +113,45 @@ export default async (req, res) => { await manager.transaction(async (transactionManager) => { const { key, error } = await idempotencyKeyService .withTransaction(transactionManager) - .workStage( - idempotencyKey.idempotency_key, - async (manager) => { - const order = await orderService - .withTransaction(manager) - .retrieve(returnDto.order_id, { - select: ["refunded_total", "total"], - relations: ["items"], - }) + .workStage(idempotencyKey.idempotency_key, async (manager) => { + const order = await orderService + .withTransaction(manager) + .retrieve(returnDto.order_id, { + select: ["refunded_total", "total"], + relations: ["items"], + }) - const returnObj: any = { - order_id: returnDto.order_id, - idempotency_key: idempotencyKey.idempotency_key, - items: returnDto.items, - } - - if (returnDto.return_shipping) { - returnObj.shipping_method = returnDto.return_shipping - } - - const createdReturn = await returnService - .withTransaction(manager) - .create(returnObj) - - if (returnDto.return_shipping) { - await returnService - .withTransaction(manager) - .fulfill(createdReturn.id) - } - - await eventBus - .withTransaction(manager) - .emit("order.return_requested", { - id: returnDto.order_id, - return_id: createdReturn.id, - }) - - return { - recovery_point: "return_requested", - } + const returnObj: any = { + order_id: returnDto.order_id, + idempotency_key: idempotencyKey.idempotency_key, + items: returnDto.items, } - ) + + if (returnDto.return_shipping) { + returnObj.shipping_method = returnDto.return_shipping + } + + const createdReturn = await returnService + .withTransaction(manager) + .create(returnObj) + + if (returnDto.return_shipping) { + await returnService + .withTransaction(manager) + .fulfill(createdReturn.id) + } + + await eventBus + .withTransaction(manager) + .emit("order.return_requested", { + id: returnDto.order_id, + return_id: createdReturn.id, + }) + + return { + recovery_point: "return_requested", + } + }) if (error) { inProgress = false @@ -173,10 +167,10 @@ export default async (req, res) => { await manager.transaction(async (transactionManager) => { const { key, error } = await idempotencyKeyService .withTransaction(transactionManager) - .workStage( - idempotencyKey.idempotency_key, - async (manager) => { - let ret = await returnService.withTransaction(manager).list( + .workStage(idempotencyKey.idempotency_key, async (manager) => { + const returnOrders = await returnService + .withTransaction(manager) + .list( { idempotency_key: idempotencyKey.idempotency_key, }, @@ -184,20 +178,19 @@ export default async (req, res) => { relations: ["items", "items.reason"], } ) - if (!ret.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Return not found` - ) - } - ret = ret[0] - - return { - response_code: 200, - response_body: { return: ret }, - } + if (!returnOrders.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Return not found` + ) } - ) + const returnOrder = returnOrders[0] + + return { + response_code: 200, + response_body: { return: returnOrder }, + } + }) if (error) { inProgress = false @@ -218,14 +211,11 @@ export default async (req, res) => { await manager.transaction(async (transactionManager) => { idempotencyKey = await idempotencyKeyService .withTransaction(transactionManager) - .update( - idempotencyKey.idempotency_key, - { - recovery_point: "finished", - response_code: 500, - response_body: { message: "Unknown recovery point" }, - } - ) + .update(idempotencyKey.idempotency_key, { + recovery_point: "finished", + response_code: 500, + response_body: { message: "Unknown recovery point" }, + }) }) break } diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index b911896f98..12089add51 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -18,6 +18,7 @@ import { ClaimType, FulfillmentItem, LineItem, + ReturnItem, } from "../models" import { ClaimRepository } from "../repositories/claim" import { DeepPartial, EntityManager } from "typeorm" @@ -425,11 +426,14 @@ export default class ClaimService extends TransactionBaseService { await this.returnService_.withTransaction(transactionManager).create({ order_id: order.id, claim_order_id: result.id, - items: claim_items.map((ci) => ({ - item_id: ci.item_id, - quantity: ci.quantity, - metadata: (ci as any).metadata, - })), + items: claim_items.map( + (ci) => + ({ + item_id: ci.item_id, + quantity: ci.quantity, + metadata: (ci as any).metadata, + } as ReturnItem) + ), shipping_method: return_shipping, no_notification: evaluatedNoNotification, }) diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 9f59ebf638..ad2f18459d 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -14,6 +14,7 @@ import { LineItem } from "../models/line-item" import LineItemAdjustmentService from "./line-item-adjustment" import { Cart } from "../models/cart" import { LineItemAdjustment } from "../models/line-item-adjustment" +import { FindConfig } from "../types/common" type InjectedDependencies = { manager: EntityManager @@ -89,7 +90,11 @@ class LineItemService extends BaseService { async list( selector, - config = { skip: 0, take: 50, order: { created_at: "DESC" } } + config: FindConfig = { + skip: 0, + take: 50, + order: { created_at: "DESC" }, + } ): Promise { const manager = this.manager_ const lineItemRepo = manager.getCustomRepository(this.lineItemRepository_) diff --git a/packages/medusa/src/services/return.js b/packages/medusa/src/services/return.ts similarity index 62% rename from packages/medusa/src/services/return.js rename to packages/medusa/src/services/return.ts index 24ca142212..30308c9e8a 100644 --- a/packages/medusa/src/services/return.js +++ b/packages/medusa/src/services/return.ts @@ -1,12 +1,66 @@ +import { isDefined } from "class-validator" import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" -import { isDefined } from "../utils" +import { DeepPartial, EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" +import { + FulfillmentStatus, + LineItem, + Order, + PaymentStatus, + Return, + ReturnItem, + ReturnStatus, +} from "../models" +import { ReturnRepository } from "../repositories/return" +import { ReturnItemRepository } from "../repositories/return-item" +import { FindConfig, Selector } from "../types/common" +import { OrdersReturnItem } from "../types/orders" +import { CreateReturnInput, UpdateReturnInput } from "../types/return" +import { buildQuery, setMetadata } from "../utils" +import FulfillmentProviderService from "./fulfillment-provider" +import InventoryService from "./inventory" +import LineItemService from "./line-item" +import OrderService from "./order" +import ReturnReasonService from "./return-reason" +import ShippingOptionService from "./shipping-option" +import TaxProviderService from "./tax-provider" +import TotalsService from "./totals" + +type InjectedDependencies = { + manager: EntityManager + totalsService: TotalsService + lineItemService: LineItemService + returnRepository: typeof ReturnRepository + returnItemRepository: typeof ReturnItemRepository + shippingOptionService: ShippingOptionService + returnReasonService: ReturnReasonService + taxProviderService: TaxProviderService + fulfillmentProviderService: FulfillmentProviderService + inventoryService: InventoryService + orderService: OrderService +} + +type Transformer = ( + item?: LineItem, + quantity?: number, + additional?: OrdersReturnItem +) => Promise> | DeepPartial + +class ReturnService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly totalsService_: TotalsService + protected readonly returnRepository_: typeof ReturnRepository + protected readonly returnItemRepository_: typeof ReturnItemRepository + protected readonly lineItemService_: LineItemService + protected readonly taxProviderService_: TaxProviderService + protected readonly shippingOptionService_: ShippingOptionService + protected readonly fulfillmentProviderService_: FulfillmentProviderService + protected readonly returnReasonService_: ReturnReasonService + protected readonly inventoryService_: InventoryService + protected readonly orderService_: OrderService -/** - * Handles Returns - * @extends BaseService - */ -class ReturnService extends BaseService { constructor({ manager, totalsService, @@ -19,75 +73,54 @@ class ReturnService extends BaseService { fulfillmentProviderService, inventoryService, orderService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {TotalsService} */ - this.totalsService_ = totalsService - - /** @private @const {ReturnRepository} */ - this.returnRepository_ = returnRepository - - /** @private @const {ReturnItemRepository} */ - this.returnItemRepository_ = returnItemRepository - - /** @private @const {ReturnItemRepository} */ - this.lineItemService_ = lineItemService - - this.taxProviderService_ = taxProviderService - - /** @private @const {ShippingOptionService} */ - this.shippingOptionService_ = shippingOptionService - - /** @private @const {FulfillmentProviderService} */ - this.fulfillmentProviderService_ = fulfillmentProviderService - - this.returnReasonService_ = returnReasonService - - this.inventoryService_ = inventoryService - - /** @private @const {OrderService} */ - this.orderService_ = orderService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new ReturnService({ - manager: transactionManager, - totalsService: this.totalsService_, - lineItemService: this.lineItemService_, - returnRepository: this.returnRepository_, - taxProviderService: this.taxProviderService_, - returnItemRepository: this.returnItemRepository_, - shippingOptionService: this.shippingOptionService_, - fulfillmentProviderService: this.fulfillmentProviderService_, - returnReasonService: this.returnReasonService_, - inventoryService: this.inventoryService_, - orderService: this.orderService_, + }: InjectedDependencies) { + super({ + manager, + totalsService, + lineItemService, + returnRepository, + returnItemRepository, + shippingOptionService, + returnReasonService, + taxProviderService, + fulfillmentProviderService, + inventoryService, + orderService, }) - cloned.transactionManager_ = transactionManager - - return cloned + this.manager_ = manager + this.totalsService_ = totalsService + this.returnRepository_ = returnRepository + this.returnItemRepository_ = returnItemRepository + this.lineItemService_ = lineItemService + this.taxProviderService_ = taxProviderService + this.shippingOptionService_ = shippingOptionService + this.fulfillmentProviderService_ = fulfillmentProviderService + this.returnReasonService_ = returnReasonService + this.inventoryService_ = inventoryService + this.orderService_ = orderService } /** * Retrieves the order line items, given an array of items - * @param {Order} order - the order to get line items from - * @param {{ item_id: string, quantity: number }} items - the items to get - * @param {function} transformer - a function to apply to each of the items + * @param order - the order to get line items from + * @param items - the items to get + * @param transformer - a function to apply to each of the items * retrieved from the order, should return a line item. If the transformer * returns an undefined value the line item will be filtered from the * returned array. - * @return {Promise>} the line items generated by the transformer. + * @return the line items generated by the transformer. */ - async getFulfillmentItems_(order, items, transformer) { + protected async getFulfillmentItems( + order: Order, + items: OrdersReturnItem[], + transformer: Transformer + ): Promise< + (LineItem & { + reason_id?: string + note?: string + })[] + > { let merged = [...order.items] // merge items from order with items from order swaps @@ -110,33 +143,37 @@ class ReturnService extends BaseService { }) ) - return toReturn.filter((i) => !!i) + return toReturn.filter((i) => !!i) as (LineItem & OrdersReturnItem)[] } /** - * @param {Object} selector - the query object for find - * @param {object} config - the config object for find - * @return {Promise} the result of the find operation + * @param selector - the query object for find + * @param config - the config object for find + * @return the result of the find operation */ list( - selector, - config = { skip: 0, take: 50, order: { created_at: "DESC" } } - ) { + selector: Selector, + config: FindConfig = { + skip: 0, + take: 50, + order: { created_at: "DESC" }, + } + ): Promise { const returnRepo = this.manager_.getCustomRepository(this.returnRepository_) - const query = this.buildQuery_(selector, config) + const query = buildQuery(selector, config) return returnRepo.find(query) } /** * Cancels a return if possible. Returns can be canceled if it has not been received. - * @param {string} returnId - the id of the return to cancel. - * @return {Promise} the updated Return + * @param returnId - the id of the return to cancel. + * @return the updated Return */ - async cancel(returnId) { - return this.atomicPhase_(async (manager) => { + async cancel(returnId: string): Promise { + return await this.atomicPhase_(async (manager) => { const ret = await this.retrieve(returnId) - if (ret.status === "received") { + if (ret.status === ReturnStatus.RECEIVED) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Can't cancel a return which has been returned" @@ -145,10 +182,9 @@ class ReturnService extends BaseService { const retRepo = manager.getCustomRepository(this.returnRepository_) - ret.status = "canceled" + ret.status = ReturnStatus.CANCELED - const result = retRepo.save(ret) - return result + return await retRepo.save(ret) }) } @@ -156,13 +192,13 @@ class ReturnService extends BaseService { * Checks that an order has the statuses necessary to complete a return. * fulfillment_status cannot be not_fulfilled or returned. * payment_status must be captured. - * @param {Order} order - the order to check statuses on + * @param order - the order to check statuses on * @throws when statuses are not sufficient for returns. */ - validateReturnStatuses_(order) { + protected validateReturnStatuses(order: Order): void | never { if ( - order.fulfillment_status === "not_fulfilled" || - order.fulfillment_status === "returned" + order.fulfillment_status === FulfillmentStatus.NOT_FULFILLED || + order.fulfillment_status === FulfillmentStatus.RETURNED ) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, @@ -170,7 +206,7 @@ class ReturnService extends BaseService { ) } - if (order.payment_status !== "captured") { + if (order.payment_status !== PaymentStatus.CAPTURED) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Can't return an order with payment unprocessed" @@ -182,14 +218,18 @@ class ReturnService extends BaseService { * Checks that a given quantity of a line item can be returned. Fails if the * item is undefined or if the returnable quantity of the item is lower, than * the quantity that is requested to be returned. - * @param {LineItem?} item - the line item to check has sufficient returnable + * @param item - the line item to check has sufficient returnable * quantity. - * @param {number} quantity - the quantity that is requested to be returned. - * @param {object} additional - the quantity that is requested to be returned. - * @return {LineItem} a line item where the quantity is set to the requested + * @param quantity - the quantity that is requested to be returned. + * @param additional - the quantity that is requested to be returned. + * @return a line item where the quantity is set to the requested * return quantity. */ - validateReturnLineItem_(item, quantity, additional) { + protected validateReturnLineItem( + item?: LineItem, + quantity = 0, + additional: { reason_id?: string; note?: string } = {} + ): DeepPartial { if (!item) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -205,13 +245,13 @@ class ReturnService extends BaseService { ) } - const toReturn = { + const toReturn: DeepPartial = { ...item, quantity, } if ("reason_id" in additional) { - toReturn.reason_id = additional.reason_id + toReturn.reason_id = additional.reason_id as string } if ("note" in additional) { @@ -223,17 +263,19 @@ class ReturnService extends BaseService { /** * Retrieves a return by its id. - * @param {string} id - the id of the return to retrieve - * @param {object} config - the config object - * @return {Return} the return + * @param id - the id of the return to retrieve + * @param config - the config object + * @return the return */ - async retrieve(id, config = {}) { + async retrieve( + id: string, + config: FindConfig = {} + ): Promise { const returnRepository = this.manager_.getCustomRepository( this.returnRepository_ ) - const validatedId = this.validateId_(id) - const query = this.buildQuery_({ id: validatedId }, config) + const query = buildQuery({ id }, config) const returnObj = await returnRepository.findOne(query) @@ -246,16 +288,17 @@ class ReturnService extends BaseService { return returnObj } - async retrieveBySwap(swapId, relations = []) { + async retrieveBySwap( + swapId: string, + relations: string[] = [] + ): Promise { const returnRepository = this.manager_.getCustomRepository( this.returnRepository_ ) - const validatedId = this.validateId_(swapId) - const returnObj = await returnRepository.findOne({ where: { - swap_id: validatedId, + swap_id: swapId, }, relations, }) @@ -266,11 +309,12 @@ class ReturnService extends BaseService { `Return with swa_id: ${swapId} was not found` ) } + return returnObj } - async update(returnId, update) { - return this.atomicPhase_(async (manager) => { + async update(returnId: string, update: UpdateReturnInput): Promise { + return await this.atomicPhase_(async (manager) => { const ret = await this.retrieve(returnId) if (ret.status === "canceled") { @@ -283,7 +327,7 @@ class ReturnService extends BaseService { const { metadata, ...rest } = update if (metadata) { - ret.metadata = this.setMetadata_(ret, metadata) + ret.metadata = setMetadata(ret, metadata) } for (const [key, value] of Object.entries(rest)) { @@ -291,8 +335,7 @@ class ReturnService extends BaseService { } const retRepo = manager.getCustomRepository(this.returnRepository_) - const result = await retRepo.save(ret) - return result + return await retRepo.save(ret) }) } @@ -300,26 +343,27 @@ class ReturnService extends BaseService { * Creates a return request for an order, with given items, and a shipping * method. If no refund amount is provided the refund amount is calculated from * the return lines and the shipping cost. - * @param {object} data - data to use for the return e.g. shipping_method, + * @param data - data to use for the return e.g. shipping_method, * items or refund_amount - * @param {object} orderLike - order object - * @return {Promise} the created return + * @return the created return */ - async create(data) { - return this.atomicPhase_(async (manager) => { + async create(data: CreateReturnInput): Promise { + return await this.atomicPhase_(async (manager) => { const returnRepository = manager.getCustomRepository( this.returnRepository_ ) const orderId = data.order_id if (data.swap_id) { - delete data.order_id + delete (data as Partial).order_id } - for (const item of data.items) { - const line = await this.lineItemService_.retrieve(item.item_id, { - relations: ["order", "swap", "claim_order"], - }) + for (const item of data.items ?? []) { + const line = await this.lineItemService_ + .withTransaction(manager) + .retrieve(item.item_id, { + relations: ["order", "swap", "claim_order"], + }) if ( line.order?.canceled_at || @@ -351,10 +395,10 @@ class ReturnService extends BaseService { ], }) - const returnLines = await this.getFulfillmentItems_( + const returnLines = await this.getFulfillmentItems( order, - data.items, - this.validateReturnLineItem_ + data.items ?? [], + this.validateReturnLineItem ) let toRefund = data.refund_amount @@ -363,7 +407,7 @@ class ReturnService extends BaseService { // refundable const refundable = order.refundable_amount - if (toRefund > refundable) { + if (toRefund! > refundable) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Cannot refund more than the original payment" @@ -371,7 +415,7 @@ class ReturnService extends BaseService { } } else { // Merchant hasn't specified refund amount so we calculate it - toRefund = await this.totalsService_.getRefundTotal(order, returnLines) + toRefund = this.totalsService_.getRefundTotal(order, returnLines) } const method = data.shipping_method @@ -379,14 +423,16 @@ class ReturnService extends BaseService { const returnObject = { ...data, - status: "requested", - refund_amount: Math.floor(toRefund), + status: ReturnStatus.REQUESTED, + refund_amount: Math.floor(toRefund!), } - const returnReasons = await this.returnReasonService_.list( - { id: [...returnLines.map((rl) => rl.reason_id)] }, - { relations: ["return_reason_children"] } - ) + const returnReasons = await this.returnReasonService_ + .withTransaction(manager) + .list( + { id: [...returnLines.map((rl) => rl.reason_id as string)] }, + { relations: ["return_reason_children"] } + ) if (returnReasons.some((rr) => rr.return_reason_children?.length > 0)) { throw new MedusaError( @@ -404,14 +450,13 @@ class ReturnService extends BaseService { reason_id: i.reason_id, note: i.note, metadata: i.metadata, - no_notification: data.no_notification, }) ) - const created = await returnRepository.create(returnObject) + const created = (await returnRepository.create(returnObject)) as Return const result = await returnRepository.save(created) - if (method) { + if (method && method.option_id) { const shippingMethod = await this.shippingOptionService_ .withTransaction(manager) .createShippingMethod( @@ -439,7 +484,7 @@ class ReturnService extends BaseService { ) if (typeof data.refund_amount === "undefined") { - result.refund_amount = toRefund - shippingTotal + result.refund_amount = toRefund! - shippingTotal return await returnRepository.save(result) } } @@ -448,8 +493,8 @@ class ReturnService extends BaseService { }) } - fulfill(returnId) { - return this.atomicPhase_(async (manager) => { + async fulfill(returnId: string): Promise { + return await this.atomicPhase_(async (manager) => { const returnOrder = await this.retrieve(returnId, { relations: [ "items", @@ -470,7 +515,7 @@ class ReturnService extends BaseService { const returnData = { ...returnOrder } - const items = await this.lineItemService_.list( + const items = await this.lineItemService_.withTransaction(manager).list( { id: returnOrder.items.map(({ item_id }) => item_id), }, @@ -482,7 +527,7 @@ class ReturnService extends BaseService { return { ...item, item: found, - } + } as ReturnItem }) if (returnOrder.shipping_data) { @@ -496,14 +541,11 @@ class ReturnService extends BaseService { return returnOrder } - const fulfillmentData = + returnOrder.shipping_data = await this.fulfillmentProviderService_.createReturn(returnData) - returnOrder.shipping_data = fulfillmentData - const returnRepo = manager.getCustomRepository(this.returnRepository_) - const result = await returnRepo.save(returnOrder) - return result + return await returnRepo.save(returnOrder) }) } @@ -515,29 +557,29 @@ class ReturnService extends BaseService { * retuned items are not matching the requested items. Setting the * allowMismatch argument to true, will process the return, ignoring any * mismatches. - * @param {string} return_id - the orderId to return to - * @param {Item[]} received_items - the items received after return. - * @param {number | undefined} refund_amount - the amount to return - * @param {bool} allow_mismatch - whether to ignore return/received + * @param returnId - the orderId to return to + * @param receivedItems - the items received after return. + * @param refundAmount - the amount to return + * @param allowMismatch - whether to ignore return/received * product mismatch - * @return {Promise} the result of the update operation + * @return the result of the update operation */ async receive( - return_id, - received_items, - refund_amount, - allow_mismatch = false - ) { - return this.atomicPhase_(async (manager) => { + returnId: string, + receivedItems: OrdersReturnItem[], + refundAmount?: number, + allowMismatch = false + ): Promise { + return await this.atomicPhase_(async (manager) => { const returnRepository = manager.getCustomRepository( this.returnRepository_ ) - const returnObj = await this.retrieve(return_id, { + const returnObj = await this.retrieve(returnId, { relations: ["items", "swap", "swap.additional_items"], }) - if (returnObj.status === "canceled") { + if (returnObj.status === ReturnStatus.CANCELED) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Cannot receive a canceled return" @@ -569,17 +611,17 @@ class ReturnService extends BaseService { ], }) - if (returnObj.status === "received") { + if (returnObj.status === ReturnStatus.RECEIVED) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, - `Return with id ${return_id} has already been received` + `Return with id ${returnId} has already been received` ) } - const returnLines = await this.getFulfillmentItems_( + const returnLines = await this.getFulfillmentItems( order, - received_items, - this.validateReturnLineItem_ + receivedItems, + this.validateReturnLineItem ) const newLines = returnLines.map((l) => { @@ -604,15 +646,14 @@ class ReturnService extends BaseService { } }) - let returnStatus = "received" + let returnStatus = ReturnStatus.RECEIVED const isMatching = newLines.every((l) => l.is_requested) - if (!isMatching && !allow_mismatch) { - // Should update status - returnStatus = "requires_action" + if (!isMatching && !allowMismatch) { + returnStatus = ReturnStatus.REQUIRES_ACTION } - const totalRefundableAmount = refund_amount || returnObj.refund_amount + const totalRefundableAmount = refundAmount || returnObj.refund_amount const now = new Date() const updateObj = { diff --git a/packages/medusa/src/types/return.ts b/packages/medusa/src/types/return.ts new file mode 100644 index 0000000000..be5485607e --- /dev/null +++ b/packages/medusa/src/types/return.ts @@ -0,0 +1,30 @@ +type OrdersReturnItem = { + item_id: string + quantity: number + reason_id?: string + note?: string +} + +export type CreateReturnInput = { + order_id: string + swap_id?: string + claim_order_id?: string + items?: OrdersReturnItem[] + shipping_method?: { + option_id?: string + price?: number + } + no_notification?: boolean + metadata?: Record + refund_amount?: number +} + +export type UpdateReturnInput = { + items?: OrdersReturnItem[] + shipping_method?: { + option_id: string + price?: number + } + no_notification?: boolean + metadata?: Record +} From 8c4be3353630efd18759eb893666e44b1b49e2b7 Mon Sep 17 00:00:00 2001 From: endigo Date: Sun, 21 Aug 2022 18:12:40 +0800 Subject: [PATCH 23/34] feat(medusa): Add Mongolian currency tugrug (#2067) --- .changeset/shaggy-pots-explain.md | 5 +++++ packages/medusa/src/utils/currencies.ts | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/shaggy-pots-explain.md diff --git a/.changeset/shaggy-pots-explain.md b/.changeset/shaggy-pots-explain.md new file mode 100644 index 0000000000..cf041b5bba --- /dev/null +++ b/.changeset/shaggy-pots-explain.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +add Mongolian native currency tugrug diff --git a/packages/medusa/src/utils/currencies.ts b/packages/medusa/src/utils/currencies.ts index 81d4e75205..fafcce7d6e 100644 --- a/packages/medusa/src/utils/currencies.ts +++ b/packages/medusa/src/utils/currencies.ts @@ -648,6 +648,15 @@ export const currencies: Record = { code: "MMK", name_plural: "Myanma kyats", }, + MNT: { + symbol: "MNT", + name: "Mongolian Tugrig", + symbol_native: "₮", + decimal_digits: 0, + rounding: 0, + code: "MNT", + name_plural: "Mongolian Tugrugs", + }, MOP: { symbol: "MOP$", name: "Macanese Pataca", From 448fd5b44e7899c624a03801f21c4c86c9f7ef6b Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 21 Aug 2022 17:54:30 +0700 Subject: [PATCH 24/34] tests(integration-tests): Allow null updates in discounts (#1299) --- .../api/__tests__/admin/discount.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index 6135a7b687..5a83ec7a00 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -1485,6 +1485,70 @@ describe("/admin/discounts", () => { }) }) + describe("POST /admin/discounts/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + await dbConnection.manager.insert(DiscountRule, { + id: "test-discount-rule", + description: "Test discount rule", + type: "percentage", + value: 10, + allocation: "total", + }) + await dbConnection.manager.insert(Discount, { + id: "test-discount", + code: "TESTING", + rule_id: "test-discount-rule", + is_dynamic: false, + is_disabled: false, + ends_at: new Date(), + usage_limit: 10, + valid_duration: "P1D", + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("Removes ends_at, valid_duration and usage_limit when fields are updated with null", async () => { + const api = useApi() + + await api + .post( + "/admin/discounts/test-discount", + { + ends_at: null, + valid_duration: null, + usage_limit: null, + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) + + const resultingDiscount = await api.get( + "/admin/discounts/test-discount", + { headers: { Authorization: "Bearer test_token" } } + ) + + expect(resultingDiscount.status).toEqual(200) + expect(resultingDiscount.data.discount).toEqual( + expect.objectContaining({ + ends_at: null, + valid_duration: null, + usage_limit: null, + }) + ) + }) + }) + describe("testing for soft-deletion + uniqueness on discount codes", () => { let manager beforeEach(async () => { From a54dc68db7a7d476cf4bf8d36c122c7f34629c90 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 21 Aug 2022 18:26:25 +0700 Subject: [PATCH 25/34] feat(medusa): Filtering Customer Orders (#975) --- .changeset/khaki-spiders-hug.md | 5 + .../api/__tests__/store/customer.js | 148 ++++++++++++++++- .../src/api/routes/store/customers/index.ts | 17 +- .../api/routes/store/customers/list-orders.ts | 151 +++++++++++++----- packages/medusa/src/services/order.ts | 5 + packages/medusa/src/types/global.ts | 2 +- 6 files changed, 280 insertions(+), 48 deletions(-) create mode 100644 .changeset/khaki-spiders-hug.md diff --git a/.changeset/khaki-spiders-hug.md b/.changeset/khaki-spiders-hug.md new file mode 100644 index 0000000000..d6c2fc4767 --- /dev/null +++ b/.changeset/khaki-spiders-hug.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Allow filtering of customer orders diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index a3e8fdbdd4..f9d3137b42 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -1,5 +1,5 @@ const path = require("path") -const { Address, Customer } = require("@medusajs/medusa") +const { Address, Customer, Order, Region } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") @@ -19,7 +19,7 @@ describe("/store/customers", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) + medusaProcess = await setupServer({ cwd, verbose: false }) }) afterAll(async () => { @@ -89,6 +89,150 @@ describe("/store/customers", () => { }) }) + describe("GET /store/customers/me/orders", () => { + beforeEach(async () => { + const manager = dbConnection.manager + await manager.query(`ALTER SEQUENCE order_display_id_seq RESTART WITH 1`) + + await manager.insert(Address, { + id: "addr_test", + first_name: "String", + last_name: "Stringson", + address_1: "String st", + city: "Stringville", + postal_code: "1236", + province: "ca", + country_code: "us", + }) + + await manager.insert(Region, { + id: "region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + await manager.insert(Customer, { + id: "test_customer", + first_name: "John", + last_name: "Deere", + email: "john@deere.com", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + }) + + await manager.insert(Customer, { + id: "test_customer1", + first_name: "John", + last_name: "Deere", + email: "joh1n@deere.com", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + }) + + await manager.insert(Order, { + id: "order_test_completed", + email: "test1@email.com", + display_id: 1, + customer_id: "test_customer", + region_id: "region", + status: "completed", + tax_rate: 0, + currency_code: "usd", + }) + + await manager.insert(Order, { + id: "order_test_completed1", + email: "test1@email.com", + display_id: 2, + customer_id: "test_customer1", + region_id: "region", + status: "completed", + tax_rate: 0, + currency_code: "usd", + }) + + await manager.insert(Order, { + id: "order_test_canceled", + email: "test1@email.com", + display_id: 3, + customer_id: "test_customer", + region_id: "region", + status: "canceled", + tax_rate: 0, + currency_code: "usd", + }) + }) + + afterEach(async () => { + await doAfterEach() + }) + + it("looks up completed orders", async () => { + const api = useApi() + + const authResponse = await api.post("/store/auth", { + email: "john@deere.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const response = await api + .get("/store/customers/me/orders?status[]=completed", { + headers: { + Cookie: authCookie, + }, + }) + .catch((err) => { + return err.response + }) + expect(response.status).toEqual(200) + expect(response.data.orders[0].display_id).toEqual(1) + expect(response.data.orders[0].email).toEqual("test1@email.com") + expect(response.data.orders.length).toEqual(1) + }) + + it("looks up cancelled and completed orders", async () => { + const api = useApi() + + const authResponse = await api.post("/store/auth", { + email: "john@deere.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const response = await api + .get( + "/store/customers/me/orders?status[]=completed&status[]=canceled", + { + headers: { + Cookie: authCookie, + }, + } + ) + .catch((err) => { + return console.log(err.response.data.message) + }) + + expect(response.status).toEqual(200) + expect(response.data.orders).toEqual([ + expect.objectContaining({ + display_id: 3, + status: "canceled", + }), + expect.objectContaining({ + display_id: 1, + status: "completed", + }), + ]) + expect(response.data.orders.length).toEqual(2) + }) + }) + describe("POST /store/customers/me", () => { beforeEach(async () => { const manager = dbConnection.manager diff --git a/packages/medusa/src/api/routes/store/customers/index.ts b/packages/medusa/src/api/routes/store/customers/index.ts index f884948201..5fcbf2d8aa 100644 --- a/packages/medusa/src/api/routes/store/customers/index.ts +++ b/packages/medusa/src/api/routes/store/customers/index.ts @@ -1,7 +1,12 @@ import { Router } from "express" import { Customer, Order } from "../../../.." import { PaginatedResponse } from "../../../../types/common" -import middlewares from "../../../middlewares" +import middlewares, { transformQuery } from "../../../middlewares" +import { StoreGetCustomersCustomerOrdersParams } from "./list-orders" +import { + defaultStoreOrdersRelations, + defaultStoreOrdersFields, +} from "../orders" const route = Router() @@ -34,7 +39,15 @@ export default (app, container) => { route.get("/me", middlewares.wrap(require("./get-customer").default)) route.post("/me", middlewares.wrap(require("./update-customer").default)) - route.get("/me/orders", middlewares.wrap(require("./list-orders").default)) + route.get( + "/me/orders", + transformQuery(StoreGetCustomersCustomerOrdersParams, { + defaultFields: defaultStoreOrdersFields, + defaultRelations: defaultStoreOrdersRelations, + isList: true, + }), + middlewares.wrap(require("./list-orders").default) + ) route.post( "/me/addresses", diff --git a/packages/medusa/src/api/routes/store/customers/list-orders.ts b/packages/medusa/src/api/routes/store/customers/list-orders.ts index 642f52d761..979219cea0 100644 --- a/packages/medusa/src/api/routes/store/customers/list-orders.ts +++ b/packages/medusa/src/api/routes/store/customers/list-orders.ts @@ -1,14 +1,20 @@ -import { IsNumber, IsOptional, IsString } from "class-validator" -import { - allowedStoreOrdersFields, - allowedStoreOrdersRelations, -} from "../orders" -import { FindConfig } from "../../../../types/common" -import { Order } from "../../../../models" - -import OrderService from "../../../../services/order" import { Type } from "class-transformer" -import { validator } from "../../../../utils/validator" +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { Request, Response } from "express" +import { MedusaError } from "medusa-core-utils" +import { + FulfillmentStatus, + OrderStatus, + PaymentStatus, +} from "../../../../models/order" +import OrderService from "../../../../services/order" +import { DateComparisonOperator } from "../../../../types/common" /** * @oas [get] /customers/me/orders @@ -17,6 +23,20 @@ import { validator } from "../../../../utils/validator" * description: "Retrieves a list of a Customer's Orders." * x-authenticated: true * parameters: + * - (query) q {string} Query used for searching orders. + * - (query) id {string} Id of the order to search for. + * - (query) status {string[]} Status to search for. + * - (query) fulfillment_status {string[]} Fulfillment status to search for. + * - (query) payment_status {string[]} Payment status to search for. + * - (query) display_id {string} Display id to search for. + * - (query) cart_id {string} to search for. + * - (query) email {string} to search for. + * - (query) region_id {string} to search for. + * - (query) currency_code {string} to search for. + * - (query) tax_rate {string} to search for. + * - (query) cancelled_at {DateComparisonOperator} Date comparison for when resulting orders was cancelled, i.e. less than, greater than etc. + * - (query) created_at {DateComparisonOperator} Date comparison for when resulting orders was created, i.e. less than, greater than etc. + * - (query) updated_at {DateComparisonOperator} Date comparison for when resulting orders was updated, i.e. less than, greater than etc. * - (query) limit=10 {integer} How many orders to return. * - (query) offset=0 {integer} The offset in the resulting orders. * - (query) fields {string} (Comma separated string) Which fields should be included in the resulting orders. @@ -44,50 +64,34 @@ import { validator } from "../../../../utils/validator" * type: integer * description: The number of items per page */ -export default async (req, res) => { - const id: string = req.user.customer_id +export default async (req: Request, res: Response) => { + const id: string | undefined = req.user?.customer_id + + if (!id) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "Not authorized to list orders" + ) + } const orderService: OrderService = req.scope.resolve("orderService") - const selector = { + req.filterableFields = { + ...req.filterableFields, customer_id: id, } - const validated = await validator( - StoreGetCustomersCustomerOrdersParams, - req.query + const [orders, count] = await orderService.listAndCount( + req.filterableFields, + req.listConfig ) - let includeFields: string[] = [] - if (validated.fields) { - includeFields = validated.fields.split(",") - includeFields = includeFields.filter((f) => - allowedStoreOrdersFields.includes(f) - ) - } + const { limit, offset } = req.validatedQuery - let expandFields: string[] = [] - if (validated.expand) { - expandFields = validated.expand.split(",") - expandFields = expandFields.filter((f) => - allowedStoreOrdersRelations.includes(f) - ) - } - - const listConfig = { - select: includeFields.length ? includeFields : allowedStoreOrdersFields, - relations: expandFields.length ? expandFields : allowedStoreOrdersRelations, - skip: validated.offset, - take: validated.limit, - order: { created_at: "DESC" }, - } as FindConfig - - const [orders, count] = await orderService.listAndCount(selector, listConfig) - - res.json({ orders, count, offset: validated.offset, limit: validated.limit }) + res.json({ orders, count, offset: offset, limit: limit }) } -export class StoreGetCustomersCustomerOrdersParams { +export class StoreGetCustomersCustomerOrdersPaginationParams { @IsOptional() @IsNumber() @Type(() => Number) @@ -106,3 +110,64 @@ export class StoreGetCustomersCustomerOrdersParams { @IsString() expand?: string } + +export class StoreGetCustomersCustomerOrdersParams extends StoreGetCustomersCustomerOrdersPaginationParams { + @IsString() + @IsOptional() + id?: string + + @IsString() + @IsOptional() + q?: string + + @IsOptional() + @IsEnum(OrderStatus, { each: true }) + status?: OrderStatus[] + + @IsOptional() + @IsEnum(FulfillmentStatus, { each: true }) + fulfillment_status?: FulfillmentStatus[] + + @IsOptional() + @IsEnum(PaymentStatus, { each: true }) + payment_status?: PaymentStatus[] + + @IsString() + @IsOptional() + display_id?: string + + @IsString() + @IsOptional() + cart_id?: string + + @IsString() + @IsOptional() + email?: string + + @IsString() + @IsOptional() + region_id?: string + + @IsString() + @IsOptional() + currency_code?: string + + @IsString() + @IsOptional() + tax_rate?: string + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + created_at?: DateComparisonOperator + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + updated_at?: DateComparisonOperator + + @ValidateNested() + @IsOptional() + @Type(() => DateComparisonOperator) + canceled_at?: DateComparisonOperator +} diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index ed2bc869b8..34d63507f1 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -182,6 +182,11 @@ class OrderService extends TransactionBaseService { ) } + /** + * @param {Object} selector - the query object for find + * @param {Object} config - the config to be used for find + * @return {Promise} the result of the find operation + */ async listAndCount( selector: QuerySelector, config: FindConfig = { diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 18f0251ab7..8e890e6e22 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -9,7 +9,7 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { - user?: (User | Customer) & { userId?: string } + user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer validatedQuery: RequestQueryFields & Record validatedBody: unknown From 8cbebef403a5ac5def1f95b2e591991cfa90b7fb Mon Sep 17 00:00:00 2001 From: Kevin Antonio Rateni Iatauro <34224657+WalkingPizza@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:08:51 +0200 Subject: [PATCH 26/34] feat(medusa-js): Add deleteSession endpoint (#1234) --- .changeset/giant-jobs-battle.md | 5 +++++ packages/medusa-js/src/resources/auth.ts | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/giant-jobs-battle.md diff --git a/.changeset/giant-jobs-battle.md b/.changeset/giant-jobs-battle.md new file mode 100644 index 0000000000..cdc6addaa9 --- /dev/null +++ b/.changeset/giant-jobs-battle.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa-js": patch +--- + +Add deleteSession endpoint diff --git a/packages/medusa-js/src/resources/auth.ts b/packages/medusa-js/src/resources/auth.ts index 7ef2512d7a..ad3a1d0cee 100644 --- a/packages/medusa-js/src/resources/auth.ts +++ b/packages/medusa-js/src/resources/auth.ts @@ -18,6 +18,15 @@ class AuthResource extends BaseResource { return this.client.request("POST", path, payload, {}, customHeaders) } + /** + * @description Removes authentication session + * @return {ResponsePromise} + */ + deleteSession(customHeaders: Record = {}): ResponsePromise { + const path = `/store/auth` + return this.client.request("DELETE", path, {}, {}, customHeaders) + } + /** * @description Retrieves an authenticated session * Usually used to check if authenticated session is alive. From c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:19:41 +0200 Subject: [PATCH 27/34] feat(medusa): Convert FulfillmentService to TypeScript (#1962) --- .changeset/new-icons-chew.md | 7 + .../src/services/manual-fulfillment.js | 2 +- .../src/fulfillment-service.js | 23 ++- .../src/services/fulfillment-provider.js | 108 ---------- .../src/services/fulfillment-provider.ts | 191 ++++++++++++++++++ .../medusa/src/services/shipping-option.ts | 4 +- .../medusa/src/types/fulfillment-provider.ts | 8 + 7 files changed, 226 insertions(+), 117 deletions(-) create mode 100644 .changeset/new-icons-chew.md delete mode 100644 packages/medusa/src/services/fulfillment-provider.js create mode 100644 packages/medusa/src/services/fulfillment-provider.ts create mode 100644 packages/medusa/src/types/fulfillment-provider.ts diff --git a/.changeset/new-icons-chew.md b/.changeset/new-icons-chew.md new file mode 100644 index 0000000000..269b235443 --- /dev/null +++ b/.changeset/new-icons-chew.md @@ -0,0 +1,7 @@ +--- +"medusa-fulfillment-manual": patch +"medusa-interfaces": patch +"@medusajs/medusa": patch +--- + +Convert FulfillmentService to TypeScript diff --git a/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js b/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js index a03d5499e7..c0b5ddedc1 100644 --- a/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js +++ b/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js @@ -19,7 +19,7 @@ class ManualFulfillmentService extends FulfillmentService { ] } - validateFulfillmentData(data, cart) { + validateFulfillmentData(_, data, cart) { return data } diff --git a/packages/medusa-interfaces/src/fulfillment-service.js b/packages/medusa-interfaces/src/fulfillment-service.js index 4e56063a63..912480a0b0 100644 --- a/packages/medusa-interfaces/src/fulfillment-service.js +++ b/packages/medusa-interfaces/src/fulfillment-service.js @@ -24,7 +24,9 @@ class BaseFulfillmentService extends BaseService { * to create shipping options in Medusa that can be chosen between by the * customer. */ - getFulfillmentOptions() {} + getFulfillmentOptions() { + throw Error("getFulfillmentOptions must be overridden by the child class") + } /** * Called before a shipping method is set on a cart to ensure that the data @@ -32,12 +34,13 @@ class BaseFulfillmentService extends BaseService { * data about the shipment such as an id of a drop point. It is up to the * fulfillment provider to enforce that the correct data is being sent * through. + * @param {object} optionData - the data to validate * @param {object} data - the data to validate - * @param {object} cart - the cart to which the shipping method will be applied + * @param {object | undefined} cart - the cart to which the shipping method will be applied * @return {object} the data to populate `cart.shipping_methods.$.data` this * is usually important for future actions like generating shipping labels */ - validateFulfillmentData(data, cart) { + validateFulfillmentData(optionData, data, cart) { throw Error("validateFulfillmentData must be overridden by the child class") } @@ -56,12 +59,16 @@ class BaseFulfillmentService extends BaseService { /** * Used to calculate a price for a given shipping option. */ - calculatePrice(data, cart) { + calculatePrice(optionData, data, cart) { throw Error("calculatePrice must be overridden by the child class") } - createFulfillment() { - throw Error("createOrder must be overridden by the child class") + createFulfillment(data, items, order, fulfillment) { + throw Error("createFulfillment must be overridden by the child class") + } + + cancelFulfillment(fulfillment) { + throw Error("cancelFulfillment must be overridden by the child class") } /** @@ -94,6 +101,10 @@ class BaseFulfillmentService extends BaseService { getShipmentDocuments(data) { return [] } + + retrieveDocuments(fulfillmentData, documentType) { + throw Error("retrieveDocuments must be overridden by the child class") + } } export default BaseFulfillmentService diff --git a/packages/medusa/src/services/fulfillment-provider.js b/packages/medusa/src/services/fulfillment-provider.js deleted file mode 100644 index cc6e5ffca4..0000000000 --- a/packages/medusa/src/services/fulfillment-provider.js +++ /dev/null @@ -1,108 +0,0 @@ -import { MedusaError } from "medusa-core-utils" - -/** - * Helps retrive fulfillment providers - */ -class FulfillmentProviderService { - constructor(container) { - /** @private {logger} */ - this.container_ = container - } - - async registerInstalledProviders(providers) { - const { manager, fulfillmentProviderRepository } = this.container_ - const model = manager.getCustomRepository(fulfillmentProviderRepository) - await model.update({}, { is_installed: false }) - - for (const p of providers) { - const n = model.create({ id: p, is_installed: true }) - await model.save(n) - } - } - - async list() { - const { manager, fulfillmentProviderRepository } = this.container_ - const fpRepo = manager.getCustomRepository(fulfillmentProviderRepository) - - return await fpRepo.find({}) - } - - async listFulfillmentOptions(providers) { - const result = await Promise.all( - providers.map(async (p) => { - const provider = await this.retrieveProvider(p) - return { - provider_id: p, - options: await provider.getFulfillmentOptions(), - } - }) - ) - - return result - } - - /** - * @param {string} provider_id - the provider id - * @return {FulfillmentService} the payment fulfillment provider - */ - retrieveProvider(provider_id) { - try { - return this.container_[`fp_${provider_id}`] - } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Could not find a fulfillment provider with id: ${provider_id}` - ) - } - } - - async createFulfillment(method, items, order, fulfillment) { - const provider = this.retrieveProvider(method.shipping_option.provider_id) - return provider.createFulfillment(method.data, items, order, fulfillment) - } - - async canCalculate(option) { - const provider = this.retrieveProvider(option.provider_id) - return provider.canCalculate(option.data) - } - - async validateFulfillmentData(option, data, cart) { - const provider = this.retrieveProvider(option.provider_id) - return provider.validateFulfillmentData(option.data, data, cart) - } - - async cancelFulfillment(fulfillment) { - const provider = this.retrieveProvider(fulfillment.provider_id) - return provider.cancelFulfillment(fulfillment.data) - } - - async calculatePrice(option, data, cart) { - const provider = this.retrieveProvider(option.provider_id) - return provider.calculatePrice(option.data, data, cart) - } - - async validateOption(option) { - const provider = this.retrieveProvider(option.provider_id) - return provider.validateOption(option.data) - } - - async createReturn(returnOrder) { - const option = returnOrder.shipping_method.shipping_option - const provider = this.retrieveProvider(option.provider_id) - return provider.createReturn(returnOrder) - } - - /** - * Fetches documents from the fulfillment provider - * @param {string} providerId - the id of the provider - * @param {object} fulfillmentData - the data relating to the fulfillment - * @param {"invoice" | "label"} documentType - the typ of - * document to fetch - */ - async retrieveDocuments(providerId, fulfillmentData, documentType) { - const provider = this.retrieveProvider(providerId) - return provider.retrieveDocuments(fulfillmentData, documentType) - } -} - -export default FulfillmentProviderService diff --git a/packages/medusa/src/services/fulfillment-provider.ts b/packages/medusa/src/services/fulfillment-provider.ts new file mode 100644 index 0000000000..e58181d921 --- /dev/null +++ b/packages/medusa/src/services/fulfillment-provider.ts @@ -0,0 +1,191 @@ +import { MedusaError } from "medusa-core-utils" +import BaseFulfillmentService from "medusa-interfaces/dist/fulfillment-service" +import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" +import { + Cart, + Fulfillment, + FulfillmentProvider, + LineItem, + Order, + Return, + ShippingMethod, + ShippingOption, +} from "../models" +import { FulfillmentProviderRepository } from "../repositories/fulfillment-provider" +import { CreateFulfillmentOrder } from "../types/fulfillment" +import { + CreateReturnType, + FulfillmentOptions, +} from "../types/fulfillment-provider" +import { MedusaContainer } from "../types/global" + +type FulfillmentProviderKey = `fp_${string}` + +type FulfillmentProviderContainer = MedusaContainer & { + fulfillmentProviderRepository: typeof FulfillmentProviderRepository + manager: EntityManager +} & { + [key in `${FulfillmentProviderKey}`]: BaseFulfillmentService +} + +/** + * Helps retrive fulfillment providers + */ +class FulfillmentProviderService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly container_: FulfillmentProviderContainer + + protected readonly fulfillmentProviderRepository_: typeof FulfillmentProviderRepository + + constructor(container: FulfillmentProviderContainer) { + super(container) + + const { manager, fulfillmentProviderRepository } = container + + this.container_ = container + this.manager_ = manager + this.fulfillmentProviderRepository_ = fulfillmentProviderRepository + } + + async registerInstalledProviders(providers: string[]): Promise { + return await this.atomicPhase_(async (manager) => { + const fulfillmentProviderRepo = manager.getCustomRepository( + this.fulfillmentProviderRepository_ + ) + await fulfillmentProviderRepo.update({}, { is_installed: false }) + + for (const p of providers) { + const n = fulfillmentProviderRepo.create({ id: p, is_installed: true }) + await fulfillmentProviderRepo.save(n) + } + }) + } + + async list(): Promise { + const fpRepo = this.manager_.getCustomRepository( + this.fulfillmentProviderRepository_ + ) + + return await fpRepo.find({}) + } + + async listFulfillmentOptions( + providerIds: string[] + ): Promise { + return await Promise.all( + providerIds.map(async (p) => { + const provider = await this.retrieveProvider(p) + return { + provider_id: p, + options: + (await provider.getFulfillmentOptions()) as unknown as Record< + string, + unknown + >[], + } + }) + ) + } + + /** + * @param providerId - the provider id + * @return the payment fulfillment provider + */ + retrieveProvider(providerId: string): BaseFulfillmentService { + try { + return this.container_[`fp_${providerId}`] + } catch (err) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Could not find a fulfillment provider with id: ${providerId}` + ) + } + } + + async createFulfillment( + method: ShippingMethod, + items: LineItem[], + order: CreateFulfillmentOrder, + fulfillment: Omit + ): Promise> { + const provider = this.retrieveProvider(method.shipping_option.provider_id) + return provider.createFulfillment( + method.data, + items, + order, + fulfillment + ) as unknown as Record + } + + async canCalculate(option: ShippingOption): Promise { + const provider = this.retrieveProvider(option.provider_id) + return provider.canCalculate(option.data) as unknown as boolean + } + + async validateFulfillmentData( + option: ShippingOption, + data: Record, + cart: Cart | Record + ): Promise> { + const provider = this.retrieveProvider(option.provider_id) + return provider.validateFulfillmentData( + option.data, + data, + cart + ) as unknown as Record + } + + async cancelFulfillment(fulfillment: Fulfillment): Promise { + const provider = this.retrieveProvider(fulfillment.provider_id) + return provider.cancelFulfillment( + fulfillment.data + ) as unknown as Fulfillment + } + + async calculatePrice( + option: ShippingOption, + data: Record, + cart?: Order | Cart + ): Promise { + const provider = this.retrieveProvider(option.provider_id) + return provider.calculatePrice(option.data, data, cart) as unknown as number + } + + async validateOption(option: ShippingOption): Promise { + const provider = this.retrieveProvider(option.provider_id) + return provider.validateOption(option.data) as unknown as boolean + } + + async createReturn( + returnOrder: CreateReturnType + ): Promise> { + const option = returnOrder.shipping_method.shipping_option + const provider = this.retrieveProvider(option.provider_id) + return provider.createReturn(returnOrder) as unknown as Record< + string, + unknown + > + } + + /** + * Fetches documents from the fulfillment provider + * @param providerId - the id of the provider + * @param fulfillmentData - the data relating to the fulfillment + * @param documentType - the typ of + * @returns document to fetch + */ + // TODO: consider removal in favor of "getReturnDocuments" and "getShipmentDocuments" + async retrieveDocuments( + providerId: string, + fulfillmentData: Record, + documentType: "invoice" | "label" + ): Promise { + const provider = this.retrieveProvider(providerId) + return provider.retrieveDocuments(fulfillmentData, documentType) + } +} + +export default FulfillmentProviderService diff --git a/packages/medusa/src/services/shipping-option.ts b/packages/medusa/src/services/shipping-option.ts index a11a68ae94..83dee51501 100644 --- a/packages/medusa/src/services/shipping-option.ts +++ b/packages/medusa/src/services/shipping-option.ts @@ -252,7 +252,7 @@ class ShippingOptionService extends TransactionBaseService { */ async createShippingMethod( optionId: string, - data: object, + data: Record, config: CreateShippingMethodDto ): Promise { return await this.atomicPhase_(async (manager) => { @@ -678,7 +678,7 @@ class ShippingOptionService extends TransactionBaseService { */ async getPrice_( option: ShippingOption, - data: object, + data: Record, cart: Cart | Order | undefined ): Promise { if (option.price_type === "calculated") { diff --git a/packages/medusa/src/types/fulfillment-provider.ts b/packages/medusa/src/types/fulfillment-provider.ts new file mode 100644 index 0000000000..641bcba459 --- /dev/null +++ b/packages/medusa/src/types/fulfillment-provider.ts @@ -0,0 +1,8 @@ +import { Return } from "../models" + +export type FulfillmentOptions = { + provider_id: string + options: Record[] +} + +export type CreateReturnType = Omit From 80e02130b4a444287920989654b607f07dd8d4f8 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:37:25 +0200 Subject: [PATCH 28/34] feat(medusa): Convert SystemPaymentProvider to TypeScript (#1988) **What** - convert system payment provider to typescript Fixes CORE-397 Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/thick-cows-guess.md | 5 ++ .../src/services/system-payment-provider.js | 51 ----------------- .../src/services/system-payment-provider.ts | 55 +++++++++++++++++++ 3 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 .changeset/thick-cows-guess.md delete mode 100644 packages/medusa/src/services/system-payment-provider.js create mode 100644 packages/medusa/src/services/system-payment-provider.ts diff --git a/.changeset/thick-cows-guess.md b/.changeset/thick-cows-guess.md new file mode 100644 index 0000000000..5f78a67373 --- /dev/null +++ b/.changeset/thick-cows-guess.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Convert SystemPaymentProvider to TypeScript diff --git a/packages/medusa/src/services/system-payment-provider.js b/packages/medusa/src/services/system-payment-provider.js deleted file mode 100644 index 737c7490a0..0000000000 --- a/packages/medusa/src/services/system-payment-provider.js +++ /dev/null @@ -1,51 +0,0 @@ -import { TransactionBaseService } from "../interfaces" - -class SystemProviderService extends TransactionBaseService { - static identifier = "system" - - constructor(_) { - super(_) - } - - async createPayment(_) { - return {} - } - - async getStatus(_) { - return "authorized" - } - - async getPaymentData(_) { - return {} - } - - async authorizePayment(_) { - return { data: {}, status: "authorized" } - } - - async updatePaymentData(_) { - return {} - } - - async updatePayment(_) { - return {} - } - - async deletePayment(_) { - return {} - } - - async capturePayment(_) { - return {} - } - - async refundPayment(_) { - return {} - } - - async cancelPayment(_) { - return {} - } -} - -export default SystemProviderService diff --git a/packages/medusa/src/services/system-payment-provider.ts b/packages/medusa/src/services/system-payment-provider.ts new file mode 100644 index 0000000000..65ae67fc7d --- /dev/null +++ b/packages/medusa/src/services/system-payment-provider.ts @@ -0,0 +1,55 @@ +import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces/transaction-base-service" + +class SystemProviderService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + static identifier = "system" + + constructor(_) { + super(_) + } + + async createPayment(_): Promise> { + return {} + } + + async getStatus(_): Promise { + return "authorized" + } + + async getPaymentData(_): Promise> { + return {} + } + + async authorizePayment(_): Promise> { + return { data: {}, status: "authorized" } + } + + async updatePaymentData(_): Promise> { + return {} + } + + async updatePayment(_): Promise> { + return {} + } + + async deletePayment(_): Promise> { + return {} + } + + async capturePayment(_): Promise> { + return {} + } + + async refundPayment(_): Promise> { + return {} + } + + async cancelPayment(_): Promise> { + return {} + } +} + +export default SystemProviderService From bda83a84bc99a4741da2076f59071c177bc5534f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:59:02 +0200 Subject: [PATCH 29/34] feat(medusa): Convert RegionService to TypeScript (#1914) --- .changeset/shaggy-steaks-change.md | 5 + .../api/__tests__/admin/discount.js | 1 - .../api/routes/admin/regions/create-region.ts | 18 +- .../src/api/routes/admin/regions/index.ts | 2 +- .../api/routes/admin/regions/update-region.ts | 11 +- .../__tests__/{region.js => region.ts} | 322 +++++++-------- packages/medusa/src/services/cart.ts | 7 +- packages/medusa/src/services/pricing.ts | 2 +- .../medusa/src/services/product-variant.ts | 2 +- .../src/services/{region.js => region.ts} | 381 +++++++++++------- packages/medusa/src/types/region.ts | 27 ++ 11 files changed, 440 insertions(+), 338 deletions(-) create mode 100644 .changeset/shaggy-steaks-change.md rename packages/medusa/src/services/__tests__/{region.js => region.ts} (77%) rename packages/medusa/src/services/{region.js => region.ts} (62%) create mode 100644 packages/medusa/src/types/region.ts diff --git a/.changeset/shaggy-steaks-change.md b/.changeset/shaggy-steaks-change.md new file mode 100644 index 0000000000..8d3342d1cc --- /dev/null +++ b/.changeset/shaggy-steaks-change.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Convert RegionService to TypeScript diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index 5a83ec7a00..0e8ea631f7 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -2309,7 +2309,6 @@ describe("/admin/discounts", () => { }) const cond = discountCondition.data.discount_condition - console.log(cond.products) expect(discountCondition.status).toEqual(200) expect(cond).toMatchSnapshot({ diff --git a/packages/medusa/src/api/routes/admin/regions/create-region.ts b/packages/medusa/src/api/routes/admin/regions/create-region.ts index 80960b81f3..6027d52209 100644 --- a/packages/medusa/src/api/routes/admin/regions/create-region.ts +++ b/packages/medusa/src/api/routes/admin/regions/create-region.ts @@ -1,10 +1,16 @@ -import { IsArray, IsNumber, IsOptional, IsString } from "class-validator" -import { defaultAdminRegionFields, defaultAdminRegionRelations } from "." - +import { + IsArray, + IsNumber, + IsObject, + IsOptional, + IsString, +} from "class-validator" import { EntityManager } from "typeorm" + +import { validator } from "../../../../utils/validator" import { Region } from "../../../.." import RegionService from "../../../../services/region" -import { validator } from "../../../../utils/validator" +import { defaultAdminRegionRelations, defaultAdminRegionFields } from "." /** * @oas [post] /regions @@ -113,4 +119,8 @@ export class AdminPostRegionsReq { @IsArray() @IsString({ each: true }) countries: string[] + + @IsObject() + @IsOptional() + metadata?: Record } diff --git a/packages/medusa/src/api/routes/admin/regions/index.ts b/packages/medusa/src/api/routes/admin/regions/index.ts index 4fffb786a1..d8abb85286 100644 --- a/packages/medusa/src/api/routes/admin/regions/index.ts +++ b/packages/medusa/src/api/routes/admin/regions/index.ts @@ -58,7 +58,7 @@ export default (app) => { return app } -export const defaultAdminRegionFields = [ +export const defaultAdminRegionFields: (keyof Region)[] = [ "id", "name", "automatic_taxes", diff --git a/packages/medusa/src/api/routes/admin/regions/update-region.ts b/packages/medusa/src/api/routes/admin/regions/update-region.ts index fa67f12a10..7cbce73dfe 100644 --- a/packages/medusa/src/api/routes/admin/regions/update-region.ts +++ b/packages/medusa/src/api/routes/admin/regions/update-region.ts @@ -2,14 +2,15 @@ import { IsArray, IsBoolean, IsNumber, + IsObject, IsOptional, IsString, } from "class-validator" -import { defaultAdminRegionFields, defaultAdminRegionRelations } from "." - import { EntityManager } from "typeorm" -import RegionService from "../../../../services/region" + import { validator } from "../../../../utils/validator" +import RegionService from "../../../../services/region" +import { defaultAdminRegionRelations, defaultAdminRegionFields } from "." /** * @oas [post] /regions/{id} @@ -139,4 +140,8 @@ export class AdminPostRegionsRegionReq { @IsString({ each: true }) @IsOptional() countries?: string[] + + @IsObject() + @IsOptional() + metadata?: Record } diff --git a/packages/medusa/src/services/__tests__/region.js b/packages/medusa/src/services/__tests__/region.ts similarity index 77% rename from packages/medusa/src/services/__tests__/region.js rename to packages/medusa/src/services/__tests__/region.ts index 7b1b573c24..921fd8999b 100644 --- a/packages/medusa/src/services/__tests__/region.js +++ b/packages/medusa/src/services/__tests__/region.ts @@ -1,69 +1,83 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import RegionService from "../region" +import { + EventBusService, + FulfillmentProviderService, + PaymentProviderService, + StoreService, +} from "../index" +import { CreateRegionInput } from "../../types/region" const eventBusService = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, -} +} as unknown as EventBusService describe("RegionService", () => { + const regionRepository = MockRepository({}) + + const ppRepository = MockRepository({ + findOne: (query) => { + if (query.where.id === "should_fail") { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: "default_provider", + }) + }, + }) + const fpRepository = MockRepository({ + findOne: (query) => { + if (query.where.id === "should_fail") { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: "default_provider", + }) + }, + }) + const countryRepository = MockRepository({ + findOne: (query) => { + if (query.where.iso_2 === "dk") { + return Promise.resolve({ + id: IdMap.getId("dk"), + name: "Denmark", + display_name: "Denmark", + region_id: IdMap.getId("dk-reg"), + }) + } + return Promise.resolve({ + id: IdMap.getId("test-country"), + name: "World", + }) + }, + }) + + const currencyRepository = MockRepository({ + findOne: () => Promise.resolve({ code: "usd" }), + }) + + const storeService = { + withTransaction: function () { + return this + }, + retrieve: () => { + return { + id: IdMap.getId("test-store"), + currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], + } + }, + } as unknown as StoreService + + const taxProviderRepository = MockRepository({}) + + const fulfillmentProviderService = {} as FulfillmentProviderService + + const paymentProviderService = {} as PaymentProviderService + describe("create", () => { - const regionRepository = MockRepository({}) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const countryRepository = MockRepository({ - findOne: (query) => { - if (query.where.iso_2 === "dk") { - return Promise.resolve({ - id: IdMap.getId("dk"), - name: "Denmark", - display_name: "Denmark", - region_id: IdMap.getId("dk-reg"), - }) - } - return Promise.resolve({ - id: IdMap.getId("test-country"), - name: "World", - }) - }, - }) - - const currencyRepository = MockRepository({ - findOne: () => Promise.resolve({ code: "usd" }), - }) - - const storeService = { - withTransaction: function() { - return this - }, - retrieve: () => { - return { - id: IdMap.getId("test-store"), - currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], - } - }, - } - const regionService = new RegionService({ manager: MockManager, eventBusService, @@ -73,6 +87,9 @@ describe("RegionService", () => { regionRepository, countryRepository, storeService, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, }) beforeEach(async () => { @@ -85,7 +102,7 @@ describe("RegionService", () => { currency_code: "USD", tax_rate: 0.25, countries: ["US"], - }) + } as CreateRegionInput) expect(regionRepository.create).toHaveBeenCalledTimes(1) expect(regionRepository.create).toHaveBeenCalledWith({ @@ -106,7 +123,7 @@ describe("RegionService", () => { currency_code: "EUR", tax_rate: 0.25, countries: ["DK"], - }) + } as CreateRegionInput) } catch (error) { expect(error.message).toBe( `Denmark already exists in region ${IdMap.getId("dk-reg")}` @@ -149,7 +166,7 @@ describe("RegionService", () => { tax_rate: 0.25, countries: ["US"], payment_providers: ["should_fail"], - }) + } as CreateRegionInput) } catch (error) { expect(error.message).toBe("Payment provider not found") } @@ -163,7 +180,7 @@ describe("RegionService", () => { tax_rate: 0.25, countries: ["US"], fulfillment_providers: ["should_fail"], - }) + } as CreateRegionInput) } catch (error) { expect(error.message).toBe("Fulfillment provider not found") } @@ -179,6 +196,14 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, + countryRepository, + storeService, }) beforeEach(async () => { @@ -196,27 +221,6 @@ describe("RegionService", () => { }) describe("validateFields_", () => { - const regionRepository = MockRepository({}) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) const countryRepository = MockRepository({ findOne: (query) => { if (query.where.iso_2 === "dk") { @@ -234,24 +238,16 @@ describe("RegionService", () => { }, }) - const storeService = { - withTransaction: function() { - return this - }, - retrieve: () => { - return { - id: IdMap.getId("test-store"), - currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], - } - }, - } - const regionService = new RegionService({ manager: MockManager, eventBusService, + regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, fulfillmentProviderRepository: fpRepository, paymentProviderRepository: ppRepository, - regionRepository, + currencyRepository, countryRepository, storeService, }) @@ -262,13 +258,19 @@ describe("RegionService", () => { it("throws on invalid country code", async () => { await expect( - regionService.validateFields_({ countries: ["ddd"] }) + // @ts-ignore + regionService.validateFields({ + countries: ["ddd"], + } as CreateRegionInput) ).rejects.toThrow("Invalid country code") }) it("throws on in use country code", async () => { await expect( - regionService.validateFields_({ countries: ["DK"] }) + // @ts-ignore + regionService.validateFields({ + countries: ["DK"], + } as CreateRegionInput) ).rejects.toThrow( `Denmark already exists in region ${IdMap.getId("dk-reg")}` ) @@ -276,15 +278,19 @@ describe("RegionService", () => { it("throws on unknown payment providers", async () => { await expect( - regionService.validateFields_({ payment_providers: ["should_fail"] }) + // @ts-ignore + regionService.validateFields({ + payment_providers: ["should_fail"], + } as CreateRegionInput) ).rejects.toThrow("Payment provider not found") }) it("throws on unknown fulfillment providers", async () => { await expect( - regionService.validateFields_({ + // @ts-ignore + regionService.validateFields({ fulfillment_providers: ["should_fail"], - }) + } as CreateRegionInput) ).rejects.toThrow("Fulfillment provider not found") }) }) @@ -293,66 +299,18 @@ describe("RegionService", () => { const regionRepository = MockRepository({ findOne: () => Promise.resolve({ id: IdMap.getId("test-region") }), }) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const countryRepository = MockRepository({ - findOne: (query) => { - if (query.where.iso_2 === "dk") { - return Promise.resolve({ - id: IdMap.getId("dk"), - name: "Denmark", - region_id: IdMap.getId("dk-reg"), - }) - } - return Promise.resolve({ - id: IdMap.getId("test-country"), - name: "World", - }) - }, - }) - - const storeService = { - withTransaction: function() { - return this - }, - retrieve: () => { - return { - id: IdMap.getId("test-store"), - currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], - } - }, - } - - const currencyRepository = MockRepository({ - findOne: () => Promise.resolve({ code: "eur" }), - }) const regionService = new RegionService({ manager: MockManager, eventBusService, + regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, fulfillmentProviderRepository: fpRepository, paymentProviderRepository: ppRepository, - regionRepository, - countryRepository, currencyRepository, + countryRepository, storeService, }) @@ -395,14 +353,21 @@ describe("RegionService", () => { }), }) const countryRepository = MockRepository({ - findOne: (query) => Promise.resolve(), + findOne: () => Promise.resolve(), }) const regionService = new RegionService({ manager: MockManager, eventBusService, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, countryRepository, + storeService, }) beforeEach(async () => { @@ -434,6 +399,7 @@ describe("RegionService", () => { return Promise.resolve({ id: IdMap.getId("region") }) }, }) + const countryRepository = MockRepository({ findOne: (query) => { if (query.where.iso_2 === "dk") { @@ -454,7 +420,14 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, countryRepository, + storeService, }) beforeEach(async () => { @@ -499,7 +472,7 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, - }) + } as any) beforeEach(async () => { jest.clearAllMocks() @@ -524,33 +497,19 @@ describe("RegionService", () => { payment_providers: [{ id: "sweden_provider" }], }), }) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) const regionService = new RegionService({ manager: MockManager, eventBusService, + regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, fulfillmentProviderRepository: fpRepository, paymentProviderRepository: ppRepository, - regionRepository, + currencyRepository, + countryRepository, + storeService, }) beforeEach(async () => { @@ -609,8 +568,15 @@ describe("RegionService", () => { const regionService = new RegionService({ manager: MockManager, eventBusService, - fulfillmentProviderRepository: fpRepository, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, + countryRepository, + storeService, }) beforeEach(async () => { @@ -660,7 +626,7 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, - }) + } as any) beforeEach(async () => { jest.clearAllMocks() @@ -695,7 +661,7 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, - }) + } as any) beforeEach(async () => { jest.clearAllMocks() diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index b7eb19395c..826b3e735b 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -393,7 +393,7 @@ class CartService extends TransactionBaseService { } } else { if (data.shipping_address) { - if (!regCountries.includes(data.shipping_address.country_code)) { + if (!regCountries.includes(data.shipping_address.country_code!)) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Shipping country not in region" @@ -403,7 +403,10 @@ class CartService extends TransactionBaseService { } if (data.shipping_address_id) { const addr = await addressRepo.findOne(data.shipping_address_id) - if (addr && !regCountries.includes(addr.country_code)) { + if ( + addr?.country_code && + !regCountries.includes(addr.country_code) + ) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Shipping country not in region" diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index 5840bb55d3..6af05e28ef 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -64,7 +64,7 @@ class PricingService extends TransactionBaseService { context: PriceSelectionContext ): Promise { let automaticTaxes = false - let taxRate = null + let taxRate: number | null = null let currencyCode = context.currency_code if (context.region_id) { diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 311e2ce117..c96827ab4d 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -360,7 +360,7 @@ class ProductVariantService extends BaseService { /** * Updates a variant's prices. * Deletes any prices that are not in the update object, and is not associated with a price list. - * @param variantId - the id of variant variant + * @param variantId - the id of variant * @param prices - the update prices * @returns {Promise} empty promise */ diff --git a/packages/medusa/src/services/region.js b/packages/medusa/src/services/region.ts similarity index 62% rename from packages/medusa/src/services/region.js rename to packages/medusa/src/services/region.ts index d31d3af67c..290c955f6b 100644 --- a/packages/medusa/src/services/region.js +++ b/packages/medusa/src/services/region.ts @@ -1,18 +1,64 @@ +import { DeepPartial, EntityManager } from "typeorm" + import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" + +import StoreService from "./store" +import EventBusService from "./event-bus" import { countries } from "../utils/countries" +import { TransactionBaseService } from "../interfaces" +import { RegionRepository } from "../repositories/region" +import { CountryRepository } from "../repositories/country" +import { CurrencyRepository } from "../repositories/currency" +import { PaymentProviderRepository } from "../repositories/payment-provider" +import { FulfillmentProviderRepository } from "../repositories/fulfillment-provider" +import { TaxProviderRepository } from "../repositories/tax-provider" +import FulfillmentProviderService from "./fulfillment-provider" +import { Country, Currency, Region } from "../models" +import { FindConfig, Selector } from "../types/common" +import { CreateRegionInput, UpdateRegionInput } from "../types/region" +import { buildQuery, setMetadata } from "../utils" +import { PaymentProviderService } from "./index" + +type InjectedDependencies = { + manager: EntityManager + storeService: StoreService + eventBusService: EventBusService + paymentProviderService: PaymentProviderService + fulfillmentProviderService: FulfillmentProviderService + + regionRepository: typeof RegionRepository + countryRepository: typeof CountryRepository + currencyRepository: typeof CurrencyRepository + taxProviderRepository: typeof TaxProviderRepository + paymentProviderRepository: typeof PaymentProviderRepository + fulfillmentProviderRepository: typeof FulfillmentProviderRepository +} /** * Provides layer to manipulate regions. * @extends BaseService */ -class RegionService extends BaseService { +class RegionService extends TransactionBaseService { static Events = { UPDATED: "region.updated", CREATED: "region.created", DELETED: "region.deleted", } + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly eventBus_: EventBusService + protected readonly storeService_: StoreService + protected readonly paymentProviderService_: PaymentProviderService + protected readonly fulfillmentProviderService_: FulfillmentProviderService + protected readonly regionRepository_: typeof RegionRepository + protected readonly countryRepository_: typeof CountryRepository + protected readonly currencyRepository_: typeof CurrencyRepository + protected readonly paymentProviderRepository_: typeof PaymentProviderRepository + protected readonly fulfillmentProviderRepository_: typeof FulfillmentProviderRepository + protected readonly taxProviderRepository_: typeof TaxProviderRepository + constructor({ manager, regionRepository, @@ -25,75 +71,42 @@ class RegionService extends BaseService { taxProviderRepository, paymentProviderService, fulfillmentProviderService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {RegionRepository} */ - this.regionRepository_ = regionRepository - - /** @private @const {CountryRepository} */ - this.countryRepository_ = countryRepository - - /** @private @const {StoreService} */ - this.storeService_ = storeService - - /** @private @const {EventBus} */ - this.eventBus_ = eventBusService - - /** @private @const {CurrencyRepository} */ - this.currencyRepository_ = currencyRepository - - /** @private @const {PaymentProviderRepository} */ - this.paymentProviderRepository_ = paymentProviderRepository - - /** @private @const {FulfillmentProviderRepository} */ - this.fulfillmentProviderRepository_ = fulfillmentProviderRepository - - /** @private @const {PaymentProviderService} */ - this.paymentProviderService_ = paymentProviderService - - /** @private @const {typeof TaxProviderService} */ - this.taxProviderRepository_ = taxProviderRepository - - /** @private @const {FulfillmentProviderService} */ - this.fulfillmentProviderService_ = fulfillmentProviderService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new RegionService({ - manager: transactionManager, - regionRepository: this.regionRepository_, - currencyRepository: this.currencyRepository_, - countryRepository: this.countryRepository_, - storeService: this.storeService_, - eventBusService: this.eventBus_, - paymentProviderRepository: this.paymentProviderRepository_, - paymentProviderService: this.paymentProviderService_, - taxProviderRepository: this.taxProviderRepository_, - taxProviderService: this.taxProviderService_, - fulfillmentProviderRepository: this.fulfillmentProviderRepository_, - fulfillmentProviderService: this.fulfillmentProviderService_, + }: InjectedDependencies) { + super({ + manager, + regionRepository, + countryRepository, + storeService, + eventBusService, + currencyRepository, + paymentProviderRepository, + fulfillmentProviderRepository, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderService, }) - cloned.transactionManager_ = transactionManager - - return cloned + this.manager_ = manager + this.regionRepository_ = regionRepository + this.countryRepository_ = countryRepository + this.storeService_ = storeService + this.eventBus_ = eventBusService + this.currencyRepository_ = currencyRepository + this.paymentProviderRepository_ = paymentProviderRepository + this.fulfillmentProviderRepository_ = fulfillmentProviderRepository + this.paymentProviderService_ = paymentProviderService + this.taxProviderRepository_ = taxProviderRepository + this.fulfillmentProviderService_ = fulfillmentProviderService } /** * Creates a region. - * @param {Region} regionObject - the unvalidated region - * @return {Region} the newly created region + * + * @param data - the unvalidated region + * @return the newly created region */ - async create(regionObject) { - return this.atomicPhase_(async (manager) => { + async create(data: CreateRegionInput): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepository = manager.getCustomRepository( this.regionRepository_ ) @@ -101,13 +114,14 @@ class RegionService extends BaseService { this.currencyRepository_ ) - const { metadata, currency_code, ...toValidate } = regionObject + const regionObject = { ...data } as DeepPartial + const { metadata, currency_code, ...toValidate } = data - const validated = await this.validateFields_(toValidate) + const validated = await this.validateFields(toValidate) if (currency_code) { // will throw if currency is not added to store currencies - await this.validateCurrency_(currency_code) + await this.validateCurrency(currency_code) const currency = await currencyRepository.findOne({ where: { code: currency_code.toLowerCase() }, }) @@ -124,14 +138,17 @@ class RegionService extends BaseService { } if (metadata) { - regionObject.metadata = this.setMetadata_(regionObject, metadata) + regionObject.metadata = setMetadata( + { metadata: regionObject.metadata ?? null }, + metadata + ) } for (const [key, value] of Object.entries(validated)) { regionObject[key] = value } - const created = regionRepository.create(regionObject) + const created = regionRepository.create(regionObject) as Region const result = await regionRepository.save(created) await this.eventBus_ @@ -146,12 +163,13 @@ class RegionService extends BaseService { /** * Updates a region - * @param {string} regionId - the region to update - * @param {object} update - the data to update the region with - * @return {Promise} the result of the update operation + * + * @param regionId - the region to update + * @param update - the data to update the region with + * @return the result of the update operation */ - async update(regionId, update) { - return this.atomicPhase_(async (manager) => { + async update(regionId: string, update: UpdateRegionInput): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepository = manager.getCustomRepository( this.regionRepository_ ) @@ -163,11 +181,11 @@ class RegionService extends BaseService { const { metadata, currency_code, ...toValidate } = update - const validated = await this.validateFields_(toValidate, region.id) + const validated = await this.validateFields(toValidate, region.id) if (currency_code) { // will throw if currency is not added to store currencies - await this.validateCurrency_(currency_code) + await this.validateCurrency(currency_code) const currency = await currencyRepository.findOne({ where: { code: currency_code.toLowerCase() }, }) @@ -183,7 +201,7 @@ class RegionService extends BaseService { } if (metadata) { - region.metadata = this.setMetadata_(region, metadata) + region.metadata = setMetadata(region, metadata) } for (const [key, value] of Object.entries(validated)) { @@ -204,13 +222,19 @@ class RegionService extends BaseService { } /** - * Validates fields for creation and updates. If the region already exisits + * Validates fields for creation and updates. If the region already exists * the id can be passed to check that country updates are allowed. - * @param {object} region - the region data to validate - * @param {string?} id - optional id of the region to check against - * @return {object} the validated region data + * + * @param regionData - the region data to validate + * @param id - optional id of the region to check against + * @return the validated region data */ - async validateFields_(region, id = undefined) { + protected async validateFields< + T extends CreateRegionInput | UpdateRegionInput + >( + regionData: Omit, + id?: T extends UpdateRegionInput ? string : undefined + ): Promise> { const ppRepository = this.manager_.getCustomRepository( this.paymentProviderRepository_ ) @@ -221,23 +245,25 @@ class RegionService extends BaseService { this.taxProviderRepository_ ) + const region = { ...regionData } as DeepPartial + if (region.tax_rate) { - this.validateTaxRate_(region.tax_rate) + this.validateTaxRate(region.tax_rate) } - if (region.countries) { + if (regionData.countries) { region.countries = await Promise.all( - region.countries.map((countryCode) => - this.validateCountry_(countryCode, id) + regionData.countries!.map((countryCode) => + this.validateCountry(countryCode, id!) ) ).catch((err) => { throw err }) } - if (region.tax_provider_id) { + if ((regionData as UpdateRegionInput).tax_provider_id) { const tp = await tpRepository.findOne({ - where: { id: region.tax_provider_id }, + where: { id: (regionData as UpdateRegionInput).tax_provider_id }, }) if (!tp) { throw new MedusaError( @@ -247,9 +273,9 @@ class RegionService extends BaseService { } } - if (region.payment_providers) { + if (regionData.payment_providers) { region.payment_providers = await Promise.all( - region.payment_providers.map(async (pId) => { + regionData.payment_providers.map(async (pId) => { const pp = await ppRepository.findOne({ where: { id: pId } }) if (!pp) { throw new MedusaError( @@ -263,9 +289,9 @@ class RegionService extends BaseService { ) } - if (region.fulfillment_providers) { + if (regionData.fulfillment_providers) { region.fulfillment_providers = await Promise.all( - region.fulfillment_providers.map(async (fId) => { + regionData.fulfillment_providers.map(async (fId) => { const fp = await fpRepository.findOne({ where: { id: fId } }) if (!fp) { throw new MedusaError( @@ -284,9 +310,12 @@ class RegionService extends BaseService { /** * Validates a tax rate. Will throw if the tax rate is not between 0 and 1. - * @param {number} taxRate - a number representing the tax rate of the region + * + * @param taxRate - a number representing the tax rate of the region + * @throws if the tax rate isn't number between 0-100 + * @return void */ - validateTaxRate_(taxRate) { + protected validateTaxRate(taxRate: number): void | never { if (taxRate > 100 || taxRate < 0) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -297,9 +326,14 @@ class RegionService extends BaseService { /** * Validates a currency code. Will throw if the currency code doesn't exist. - * @param {string} currencyCode - an ISO currency code + * + * @param currencyCode - an ISO currency code + * @throws if the provided currency code is invalid + * @return void */ - async validateCurrency_(currencyCode) { + protected async validateCurrency( + currencyCode: Currency["code"] + ): Promise { const store = await this.storeService_ .withTransaction(this.transactionManager_) .retrieve({ relations: ["currencies"] }) @@ -317,10 +351,15 @@ class RegionService extends BaseService { /** * Validates a country code. Will normalize the code before checking for * existence. - * @param {string} code - a 2 digit alphanumeric ISO country code - * @param {string} regionId - the id of the current region to check against + * + * @param code - a 2 digit alphanumeric ISO country code + * @param regionId - the id of the current region to check against + * @return the validated Country */ - async validateCountry_(code, regionId) { + protected async validateCountry( + code: Country["iso_2"], + regionId: string + ): Promise { const countryRepository = this.manager_.getCustomRepository( this.countryRepository_ ) @@ -359,7 +398,17 @@ class RegionService extends BaseService { return country } - async retrieveByCountryCode(code, config = {}) { + /** + * Retrieve a region by country code. + * + * @param code - a 2 digit alphanumeric ISO country code + * @param config - region find config + * @return a Region with country code + */ + async retrieveByCountryCode( + code: Country["iso_2"], + config: FindConfig = {} + ): Promise { const countryRepository = this.manager_.getCustomRepository( this.countryRepository_ ) @@ -389,17 +438,20 @@ class RegionService extends BaseService { /** * Retrieves a region by its id. - * @param {string} regionId - the id of the region to retrieve - * @param {object} config - configuration settings - * @return {Region} the region + * + * @param regionId - the id of the region to retrieve + * @param config - configuration settings + * @return the region */ - async retrieve(regionId, config = {}) { + async retrieve( + regionId: string, + config: FindConfig = {} + ): Promise { const regionRepository = this.manager_.getCustomRepository( this.regionRepository_ ) - const validatedId = this.validateId_(regionId) - const query = this.buildQuery_({ id: validatedId }, config) + const query = buildQuery({ id: regionId }, config) const region = await regionRepository.findOne(query) if (!region) { @@ -408,29 +460,39 @@ class RegionService extends BaseService { `Region with ${regionId} was not found` ) } + return region } /** * Lists all regions based on a query + * * @param {object} selector - query object for find * @param {object} config - configuration settings * @return {Promise} result of the find operation */ - async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { + async list( + selector: Selector = {}, + config: FindConfig = { + relations: [], + skip: 0, + take: 10, + } + ): Promise { const regionRepo = this.manager_.getCustomRepository(this.regionRepository_) - const query = this.buildQuery_(selector, config) + const query = buildQuery(selector, config) return regionRepo.find(query) } /** * Deletes a region. - * @param {string} regionId - the region to delete - * @return {Promise} the result of the delete operation + * + * @param regionId - the region to delete + * @return the result of the delete operation */ - async delete(regionId) { - return this.atomicPhase_(async (manager) => { + async delete(regionId: string): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const countryRepo = manager.getCustomRepository(this.countryRepository_) @@ -455,15 +517,16 @@ class RegionService extends BaseService { /** * Adds a country to the region. - * @param {string} regionId - the region to add a country to - * @param {string} code - a 2 digit alphanumeric ISO country code. - * @return {Promise} the result of the update operation + * + * @param regionId - the region to add a country to + * @param code - a 2 digit alphanumeric ISO country code. + * @return the updated Region */ - async addCountry(regionId, code) { - return this.atomicPhase_(async (manager) => { + async addCountry(regionId: string, code: Country["iso_2"]): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) - const country = await this.validateCountry_(code, regionId) + const country = await this.validateCountry(code, regionId) const region = await this.retrieve(regionId, { relations: ["countries"] }) @@ -491,13 +554,17 @@ class RegionService extends BaseService { } /** - * Removes a country from a Region - * @param {string} regionId - the region to remove from - * @param {string} code - a 2 digit alphanumeric ISO country code to remove - * @return {Promise} the result of the update operation + * Removes a country from a Region. + * + * @param regionId - the region to remove from + * @param code - a 2 digit alphanumeric ISO country code to remove + * @return the updated Region */ - async removeCountry(regionId, code) { - return this.atomicPhase_(async (manager) => { + async removeCountry( + regionId: string, + code: Country["iso_2"] + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const region = await this.retrieve(regionId, { relations: ["countries"] }) @@ -521,6 +588,7 @@ class RegionService extends BaseService { id: updated.id, fields: ["countries"], }) + return updated }) } @@ -528,12 +596,16 @@ class RegionService extends BaseService { /** * Adds a payment provider that is available in the region. Fails if the * provider doesn't exist. - * @param {string} regionId - the region to add the provider to - * @param {string} providerId - the provider to add to the region - * @return {Promise} the result of the update operation + * + * @param regionId - the region to add the provider to + * @param providerId - the provider to add to the region + * @return the updated Region */ - async addPaymentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async addPaymentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const ppRepo = manager.getCustomRepository( this.paymentProviderRepository_ @@ -575,12 +647,16 @@ class RegionService extends BaseService { /** * Adds a fulfillment provider that is available in the region. Fails if the * provider doesn't exist. - * @param {string} regionId - the region to add the provider to - * @param {string} providerId - the provider to add to the region - * @return {Promise} the result of the update operation + * + * @param regionId - the region to add the provider to + * @param providerId - the provider to add to the region + * @return the updated Region */ - async addFulfillmentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async addFulfillmentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const fpRepo = manager.getCustomRepository( this.fulfillmentProviderRepository_ @@ -592,7 +668,7 @@ class RegionService extends BaseService { // Check if region already has payment provider if (region.fulfillment_providers.find(({ id }) => id === providerId)) { - return Promise.resolve() + return Promise.resolve(region) } const fp = await fpRepo.findOne({ where: { id: providerId } }) @@ -613,18 +689,23 @@ class RegionService extends BaseService { id: updated.id, fields: ["fulfillment_providers"], }) + return updated }) } /** * Removes a payment provider from a region. Is idempotent. - * @param {string} regionId - the region to remove the provider from - * @param {string} providerId - the provider to remove from the region - * @return {Promise} the result of the update operation + * + * @param regionId - the region to remove the provider from + * @param providerId - the provider to remove from the region + * @return the updated Region */ - async removePaymentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async removePaymentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const region = await this.retrieve(regionId, { @@ -633,7 +714,7 @@ class RegionService extends BaseService { // Check if region already has payment provider if (!region.payment_providers.find(({ id }) => id === providerId)) { - return Promise.resolve() + return Promise.resolve(region) } region.payment_providers = region.payment_providers.filter( @@ -647,18 +728,23 @@ class RegionService extends BaseService { id: updated.id, fields: ["payment_providers"], }) + return updated }) } /** * Removes a fulfillment provider from a region. Is idempotent. - * @param {string} regionId - the region to remove the provider from - * @param {string} providerId - the provider to remove from the region - * @return {Promise} the result of the update operation + * + * @param regionId - the region to remove the provider from + * @param providerId - the provider to remove from the region + * @return the updated Region */ - async removeFulfillmentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async removeFulfillmentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const region = await this.retrieve(regionId, { @@ -667,7 +753,7 @@ class RegionService extends BaseService { // Check if region already has payment provider if (!region.fulfillment_providers.find(({ id }) => id === providerId)) { - return Promise.resolve() + return Promise.resolve(region) } region.fulfillment_providers = region.fulfillment_providers.filter( @@ -681,6 +767,7 @@ class RegionService extends BaseService { id: updated.id, fields: ["fulfillment_providers"], }) + return updated }) } diff --git a/packages/medusa/src/types/region.ts b/packages/medusa/src/types/region.ts new file mode 100644 index 0000000000..89a2803356 --- /dev/null +++ b/packages/medusa/src/types/region.ts @@ -0,0 +1,27 @@ +import { FindConfig } from "./common" +import { Region } from "../models" + +export type UpdateRegionInput = { + name?: string + currency_code?: string + tax_code?: string + tax_rate?: number + gift_cards_taxable?: boolean + automatic_taxes?: boolean + tax_provider_id?: string | null + payment_providers?: string[] + fulfillment_providers?: string[] + countries?: string[] + metadata?: Record +} + +export type CreateRegionInput = { + name: string + currency_code: string + tax_code?: string + tax_rate: number + payment_providers: string[] + fulfillment_providers: string[] + countries: string[] + metadata?: Record +} From 5ac7f08e4d013491ecc4dc0ed497e665560dfd1e Mon Sep 17 00:00:00 2001 From: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Wed, 24 Aug 2022 16:07:44 +0200 Subject: [PATCH 30/34] fix(medusa): Incorrect swap difference due (#2086) ### What Creating a swap on an order with a discount leads to an incorrect difference due. **Scenario** - Create a store with minimum 2 products (Prod A, Prod B) - Create a discount that only works for Prod A - Create an order for Prod A with the discount applied - Create a swap between Prod A and Prod B **Expected outcome** We would expect the difference_due amount to come out to the sum of: - -1 * (price of prod a - discount applied to prod a) - price of prod b **Actual outcome** Instead the discount is applied across both products when calculating difference due. This results in a total that is instead the sum of: - -1 * (price of prod a - discount applied to prod a) - price of prod b - discount on prod b ignoring the condition ### How Adds `line_item.adjustments` to relations in cart retrieval prior to setting the difference_due to car total Fixes CORE-361 --- .../api/__tests__/admin/swaps.js | 215 ++++++++++++++++++ .../api/__tests__/totals/orders.js | 1 + .../simple-shipping-option-factory.ts | 6 +- integration-tests/api/package.json | 6 +- integration-tests/api/yarn.lock | 86 ++++--- packages/medusa/src/services/swap.js | 44 ++-- 6 files changed, 289 insertions(+), 69 deletions(-) diff --git a/integration-tests/api/__tests__/admin/swaps.js b/integration-tests/api/__tests__/admin/swaps.js index cf53d3c25e..e2a954c076 100644 --- a/integration-tests/api/__tests__/admin/swaps.js +++ b/integration-tests/api/__tests__/admin/swaps.js @@ -8,6 +8,17 @@ const orderSeeder = require("../../helpers/order-seeder") const swapSeeder = require("../../helpers/swap-seeder") const adminSeeder = require("../../helpers/admin-seeder") +const { + simpleProductFactory, + simpleCartFactory, + simpleDiscountFactory, + simpleRegionFactory, + simpleShippingOptionFactory, +} = require("../../factories") +const { + simpleCustomerFactory, +} = require("../../factories/simple-customer-factory") + jest.setTimeout(30000) describe("/admin/swaps", () => { @@ -144,4 +155,208 @@ describe("/admin/swaps", () => { ) }) }) + + describe("Complete swap flow", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("completes swap and ensures difference due", async () => { + // ********* FACTORIES ********* + const prodA = await simpleProductFactory(dbConnection, { + id: "prod-a", + variants: [ + { id: "prod-a-var", prices: [{ amount: 1000, currency: "dkk" }] }, + ], + }) + + await simpleProductFactory(dbConnection, { + id: "prod-b", + variants: [ + { id: "prod-b-var", prices: [{ amount: 1000, currency: "dkk" }] }, + ], + }) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + currency_code: "dkk", + }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount", + regions: ["test-region"], + code: "TEST", + rule: { + type: "percentage", + value: "10", + allocation: "total", + conditions: [ + { + type: "products", + operator: "in", + products: [prodA.id], + }, + ], + }, + }) + + await simpleCustomerFactory(dbConnection, { + id: "test-customer", + email: "test@customer.com", + }) + + const so = await simpleShippingOptionFactory(dbConnection, { + region_id: "test-region", + }) + + await simpleCartFactory(dbConnection, { + customer: "test-customer", + id: "cart-test", + line_items: [ + { + id: "line-item", + variant_id: "prod-a-var", + cart_id: "cart-test", + unit_price: 1000, + quantity: 1, + }, + ], + region: "test-region", + shipping_address: { + address_1: "test", + country_code: "us", + first_name: "chris", + last_name: "rock", + postal_code: "101", + }, + }) + + const api = useApi() + + // ********* PREPARE CART ********* + + try { + await api.post("/store/carts/cart-test", { + discounts: [{ code: "TEST" }], + }) + } catch (error) { + console.log(error) + } + + await api.post("/store/carts/cart-test/shipping-methods", { + option_id: so.id, + data: {}, + }) + await api.post("/store/carts/cart-test/payment-sessions") + const TEST = await api.post("/store/carts/cart-test/payment-session", { + provider_id: "test-pay", + }) + + console.log("Testing, ", TEST.data.cart.items[0]) + + // ********* COMPLETE CART ********* + const completedOrder = await api.post("/store/carts/cart-test/complete") + + // ********* PREPARE ORDER ********* + const orderId = completedOrder.data.data.id + const fulfilledOrder = await api.post( + `/admin/orders/${orderId}/fulfillment`, + { + items: [{ item_id: "line-item", quantity: 1 }], + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + const fulfillmentId = fulfilledOrder.data.order.fulfillments[0].id + + await api.post( + `/admin/orders/${orderId}/shipment`, + { + fulfillment_id: fulfillmentId, + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + await api.post( + `/admin/orders/${orderId}/capture`, + {}, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + // ********* CREATE SWAP ********* + const createSwap = await api.post( + `/admin/orders/${completedOrder.data.data.id}/swaps`, + { + return_items: [ + { + item_id: "line-item", + quantity: 1, + }, + ], + additional_items: [{ variant_id: "prod-b-var", quantity: 1 }], + }, + { + headers: { + authorization: "Bearer test_token", + }, + } + ) + + let swap = createSwap.data.order.swaps[0] + + // ********* PREPARE SWAP CART ********* + await api.post(`/store/carts/${swap.cart_id}/shipping-methods`, { + option_id: so.id, + data: {}, + }) + + await api.post(`/store/carts/${swap.cart_id}/payment-sessions`) + await api.post(`/store/carts/${swap.cart_id}/payment-session`, { + provider_id: "test-pay", + }) + + // ********* COMPLETE SWAP CART ********* + await api.post(`/store/carts/${swap.cart_id}/complete`) + + swap = await api + .get(`/admin/swaps/${swap.id}`, { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + const swapCart = await api.get( + `/store/carts/${swap.data.swap.cart_id}`, + {} + ) + + // ********* VALIDATE ********* + expect(swap.data.swap.difference_due).toBe(swapCart.data.cart.total) + }) + }) }) diff --git a/integration-tests/api/__tests__/totals/orders.js b/integration-tests/api/__tests__/totals/orders.js index cd642a30b7..0551dc0fd4 100644 --- a/integration-tests/api/__tests__/totals/orders.js +++ b/integration-tests/api/__tests__/totals/orders.js @@ -30,6 +30,7 @@ describe("Order Totals", () => { medusaProcess = await setupServer({ cwd }) } catch (error) { console.log(error) + throw error } }) diff --git a/integration-tests/api/factories/simple-shipping-option-factory.ts b/integration-tests/api/factories/simple-shipping-option-factory.ts index c7a1dcbf07..a85d683e4b 100644 --- a/integration-tests/api/factories/simple-shipping-option-factory.ts +++ b/integration-tests/api/factories/simple-shipping-option-factory.ts @@ -1,11 +1,11 @@ -import { Connection } from "typeorm" -import faker from "faker" import { + ShippingOption, ShippingOptionPriceType, ShippingProfile, - ShippingOption, ShippingProfileType, } from "@medusajs/medusa" +import faker from "faker" +import { Connection } from "typeorm" export type ShippingOptionFactoryData = { name?: string diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index e63e33a033..2b19e220cf 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,16 +8,16 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.3.4-dev-1658251581042", + "@medusajs/medusa": "1.3.5-dev-1661328147668", "faker": "^5.5.3", - "medusa-interfaces": "1.3.1-dev-1658251581042", + "medusa-interfaces": "1.3.2-dev-1661328147668", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.19-dev-1658251581042", + "babel-preset-medusa-package": "1.1.19-dev-1661328147668", "jest": "^26.6.3" } } diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index b9adc5d8e0..8b91d51a71 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1825,9 +1825,9 @@ __metadata: languageName: node linkType: hard -"@medusajs/medusa-cli@npm:^1.3.1": - version: 1.3.1 - resolution: "@medusajs/medusa-cli@npm:1.3.1" +"@medusajs/medusa-cli@npm:1.3.1-dev-1661328147668": + version: 1.3.1-dev-1661328147668 + resolution: "@medusajs/medusa-cli@npm:1.3.1-dev-1661328147668" dependencies: "@babel/polyfill": ^7.8.7 "@babel/runtime": ^7.9.6 @@ -1845,8 +1845,8 @@ __metadata: is-valid-path: ^0.1.1 joi-objectid: ^3.0.1 meant: ^1.0.1 - medusa-core-utils: ^0.1.27 - medusa-telemetry: ^0.0.11 + medusa-core-utils: 1.1.31-dev-1661328147668 + medusa-telemetry: 0.0.11-dev-1661328147668 netrc-parser: ^3.1.6 open: ^8.0.6 ora: ^5.4.1 @@ -1861,16 +1861,16 @@ __metadata: yargs: ^15.3.1 bin: medusa: cli.js - checksum: a58f39cdfce3fd1361944323b600fddf34f79437b01d366f5d221e4cf93204a672abdbb2a901736387f13872f1ea868e08ccc7db33038e3156f1e7b663d9f1e5 + checksum: 6f8e1d3548d6a7b987011473a9913f95291e6bb01a38f11fac43903c8831e44cb3b6d04126d5718307063f0f60fa56d78a8f88f1180f26c1a1ebf5d2ae8f0d16 languageName: node linkType: hard -"@medusajs/medusa@npm:1.3.4-dev-1658251581042": - version: 1.3.4-dev-1658251581042 - resolution: "@medusajs/medusa@npm:1.3.4-dev-1658251581042" +"@medusajs/medusa@npm:1.3.5-dev-1661328147668": + version: 1.3.5-dev-1661328147668 + resolution: "@medusajs/medusa@npm:1.3.5-dev-1661328147668" dependencies: "@hapi/joi": ^16.1.8 - "@medusajs/medusa-cli": ^1.3.1 + "@medusajs/medusa-cli": 1.3.1-dev-1661328147668 "@types/lodash": ^4.14.168 awilix: ^4.2.3 body-parser: ^1.19.0 @@ -1893,8 +1893,8 @@ __metadata: joi: ^17.3.0 joi-objectid: ^3.0.1 jsonwebtoken: ^8.5.1 - medusa-core-utils: ^1.1.31 - medusa-test-utils: ^1.1.37 + medusa-core-utils: 1.1.31-dev-1661328147668 + medusa-test-utils: 1.1.37-dev-1661328147668 morgan: ^1.9.1 multer: ^1.4.2 node-schedule: ^2.1.0 @@ -1915,11 +1915,11 @@ __metadata: uuid: ^8.3.1 winston: ^3.2.1 peerDependencies: - medusa-interfaces: 1.x + medusa-interfaces: 1.3.2 typeorm: 0.2.x bin: medusa: cli.js - checksum: 4f33990f8dd1a454a3b2d6f27dad3b2f179d91376cadc339cb6f9435339d05621602f4a3a60772662561ea0c88781efabbb1fd3b6b7c54901ec597e91077e409 + checksum: 41a155c9486f18104e184987e7119c0255b6e15d264b94800474d44f8296e6835e159efe98c0c8b804219effd54c3f1b2783d7eac01d19dbde71996b1352c56b languageName: node linkType: hard @@ -2491,11 +2491,11 @@ __metadata: "@babel/cli": ^7.12.10 "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 - "@medusajs/medusa": 1.3.4-dev-1658251581042 - babel-preset-medusa-package: 1.1.19-dev-1658251581042 + "@medusajs/medusa": 1.3.5-dev-1661328147668 + babel-preset-medusa-package: 1.1.19-dev-1661328147668 faker: ^5.5.3 jest: ^26.6.3 - medusa-interfaces: 1.3.1-dev-1658251581042 + medusa-interfaces: 1.3.2-dev-1661328147668 typeorm: ^0.2.31 languageName: unknown linkType: soft @@ -2802,9 +2802,9 @@ __metadata: languageName: node linkType: hard -"babel-preset-medusa-package@npm:1.1.19-dev-1658251581042": - version: 1.1.19-dev-1658251581042 - resolution: "babel-preset-medusa-package@npm:1.1.19-dev-1658251581042" +"babel-preset-medusa-package@npm:1.1.19-dev-1661328147668": + version: 1.1.19-dev-1661328147668 + resolution: "babel-preset-medusa-package@npm:1.1.19-dev-1661328147668" dependencies: "@babel/plugin-proposal-class-properties": ^7.12.1 "@babel/plugin-proposal-decorators": ^7.12.1 @@ -2818,7 +2818,7 @@ __metadata: core-js: ^3.7.0 peerDependencies: "@babel/core": ^7.11.6 - checksum: 55699a4aad97ed1da82a04bdfc75e16d051496ad734188e87d16dfabf1c237c829f42be054ad0a1f01d33a7943e804602f935a758b5e0a3af9f3bacffb9ec2cd + checksum: cf6bbf1400549e0641edbd2e67569bd81d3a8b611c7a56ce789784669fa77923c025d117c2105717dcc024104ad6fe14a006518368efa907c9a92fa5b09a11cb languageName: node linkType: hard @@ -6951,39 +6951,29 @@ __metadata: languageName: node linkType: hard -"medusa-core-utils@npm:^0.1.27": - version: 0.1.39 - resolution: "medusa-core-utils@npm:0.1.39" - dependencies: - "@hapi/joi": ^16.1.8 - joi-objectid: ^3.0.1 - checksum: 7c5d51de35e96312fd34e7c7b3b23cdcce197bbdad672234d766a44abe1b22cfccb28855d496b5e422558d72e87808e826ef2ef80189eaa45bdfba84942f2c8c - languageName: node - linkType: hard - -"medusa-core-utils@npm:^1.1.31, medusa-core-utils@npm:^1.1.32": - version: 1.1.32 - resolution: "medusa-core-utils@npm:1.1.32" +"medusa-core-utils@npm:1.1.31-dev-1661328147668": + version: 1.1.31-dev-1661328147668 + resolution: "medusa-core-utils@npm:1.1.31-dev-1661328147668" dependencies: joi: ^17.3.0 joi-objectid: ^3.0.1 - checksum: 6bbc326d58fcc6fb150c6fa464f3eaa87764bc3d43e4c861f1d318dd8ba1db8b6dff9e5b7624ba30d592fc870ec0857863bc07c4a8fdc12a99b668135f8cb883 + checksum: d61c4c089f8afaef3096b648d666eb41569c3d6e0bea8213fc86139c84870c836dc5f6c3fdd1d1f3031671b86ed61016227d6d090f7a9fa62b1c166198ce5bda languageName: node linkType: hard -"medusa-interfaces@npm:1.3.1-dev-1658251581042": - version: 1.3.1-dev-1658251581042 - resolution: "medusa-interfaces@npm:1.3.1-dev-1658251581042" +"medusa-interfaces@npm:1.3.2-dev-1661328147668": + version: 1.3.2-dev-1661328147668 + resolution: "medusa-interfaces@npm:1.3.2-dev-1661328147668" peerDependencies: medusa-core-utils: ^1.1.31 typeorm: 0.x - checksum: b6cbfa915233629779053cc70234c2f702c93351ed02c1230e21ad161eef8659676733f3ae12c34998d62f00296dd78a258d9b38ae0a5862cf1b8154f6f7bed3 + checksum: cc4cf53af7d5dd7fe19018c74a6d63789fd8d87696e367ffa7083d1dedd8b4a662cd6967117ede37a7a91186efcc76811c20fd1ebd4f33d23f748748027c951a languageName: node linkType: hard -"medusa-telemetry@npm:^0.0.11": - version: 0.0.11 - resolution: "medusa-telemetry@npm:0.0.11" +"medusa-telemetry@npm:0.0.11-dev-1661328147668": + version: 0.0.11-dev-1661328147668 + resolution: "medusa-telemetry@npm:0.0.11-dev-1661328147668" dependencies: axios: ^0.21.1 axios-retry: ^3.1.9 @@ -6994,18 +6984,18 @@ __metadata: is-docker: ^2.2.1 remove-trailing-slash: ^0.1.1 uuid: ^8.3.2 - checksum: f8223788eb2928b3c2bbfb29c32825216159aa062980717cc89e71904296b79907b99a514e17485436f0bd7e3b32dbc855589e8fa2cb1ecf5b75a1169ceef9d9 + checksum: 9dc2ff232dc20e49025a24d2b2aceb48294350b9a1c1f3e8042d636dd5250cbba0dd82c5c029b2c3dd43151f549898216709a176c907ac7b9c69e1a9c7a9ca6a languageName: node linkType: hard -"medusa-test-utils@npm:^1.1.37": - version: 1.1.38 - resolution: "medusa-test-utils@npm:1.1.38" +"medusa-test-utils@npm:1.1.37-dev-1661328147668": + version: 1.1.37-dev-1661328147668 + resolution: "medusa-test-utils@npm:1.1.37-dev-1661328147668" dependencies: "@babel/plugin-transform-classes": ^7.9.5 - medusa-core-utils: ^1.1.32 + medusa-core-utils: 1.1.31-dev-1661328147668 randomatic: ^3.1.1 - checksum: 613799b6bef71e857878b4b87efc6d19120cffc15171cebe116ec7b77050a3a5bfc2c53e35281d8177267281c3b10f55a0f4f5321bc098b897a3c21e978cdb4c + checksum: 23318eebf80e0b206935fa88a624638b6427fa524b5a268f0e0641089fcc702b0f631fd9a9aec631eea453e58752b1c879bef723cdb08170e30d1b8fe8b81d03 languageName: node linkType: hard diff --git a/packages/medusa/src/services/swap.js b/packages/medusa/src/services/swap.js index 58a7359508..657cdc1a58 100644 --- a/packages/medusa/src/services/swap.js +++ b/packages/medusa/src/services/swap.js @@ -184,8 +184,11 @@ class SwapService extends BaseService { const validatedId = this.validateId_(id) - const { cartSelects, cartRelations, ...newConfig } = - this.transformQueryForCart_(config) + const { + cartSelects, + cartRelations, + ...newConfig + } = this.transformQueryForCart_(config) const query = this.buildQuery_({ id: validatedId }, newConfig) @@ -600,8 +603,9 @@ class SwapService extends BaseService { }, }) - const customShippingOptionServiceTx = - this.customShippingOptionService_.withTransaction(manager) + const customShippingOptionServiceTx = this.customShippingOptionService_.withTransaction( + manager + ) for (const customShippingOption of customShippingOptions) { await customShippingOptionServiceTx.create({ cart_id: cart.id, @@ -611,8 +615,9 @@ class SwapService extends BaseService { } const lineItemServiceTx = this.lineItemService_.withTransaction(manager) - const lineItemAdjustmentServiceTx = - this.lineItemAdjustmentService_.withTransaction(manager) + const lineItemAdjustmentServiceTx = this.lineItemAdjustmentService_.withTransaction( + manager + ) for (const item of swap.additional_items) { await lineItemServiceTx.update(item.id, { cart_id: cart.id, @@ -691,7 +696,12 @@ class SwapService extends BaseService { const cart = await this.cartService_.retrieve(swap.cart_id, { select: ["total"], - relations: ["payment", "shipping_methods", "items"], + relations: [ + "payment", + "shipping_methods", + "items", + "items.adjustments", + ], }) const { payment } = cart @@ -699,10 +709,12 @@ class SwapService extends BaseService { const items = cart.items if (!swap.allow_backorder) { - const inventoryServiceTx = - this.inventoryService_.withTransaction(manager) - const paymentProviderServiceTx = - this.paymentProviderService_.withTransaction(manager) + const inventoryServiceTx = this.inventoryService_.withTransaction( + manager + ) + const paymentProviderServiceTx = this.paymentProviderService_.withTransaction( + manager + ) const cartServiceTx = this.cartService_.withTransaction(manager) for (const item of items) { @@ -750,8 +762,9 @@ class SwapService extends BaseService { order_id: swap.order_id, }) - const inventoryServiceTx = - this.inventoryService_.withTransaction(manager) + const inventoryServiceTx = this.inventoryService_.withTransaction( + manager + ) for (const item of items) { await inventoryServiceTx.adjustInventory( @@ -771,8 +784,9 @@ class SwapService extends BaseService { const swapRepo = manager.getCustomRepository(this.swapRepository_) const result = await swapRepo.save(swap) - const shippingOptionServiceTx = - this.shippingOptionService_.withTransaction(manager) + const shippingOptionServiceTx = this.shippingOptionService_.withTransaction( + manager + ) for (const method of cart.shipping_methods) { await shippingOptionServiceTx.updateShippingMethod(method.id, { From 9e0cb1212023d7035165ddd269edab3efc7ebe29 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 24 Aug 2022 16:25:40 +0200 Subject: [PATCH 31/34] fix(medusa): remove unique cart on payments to allow canceled payments to exist (#1854) Fixes CORE-321 Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/many-pumpkins-enjoy.md | 5 + .../api/__tests__/store/orders.js | 160 +++++++++++++++++- .../1661345741249-multi_payment_cart.ts | 25 +++ packages/medusa/src/models/payment.ts | 9 +- 4 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 .changeset/many-pumpkins-enjoy.md create mode 100644 packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts diff --git a/.changeset/many-pumpkins-enjoy.md b/.changeset/many-pumpkins-enjoy.md new file mode 100644 index 0000000000..2f879986a4 --- /dev/null +++ b/.changeset/many-pumpkins-enjoy.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Fixes issue where failed cart completion attempts could not be retried without 500 error diff --git a/integration-tests/api/__tests__/store/orders.js b/integration-tests/api/__tests__/store/orders.js index c6b6922d32..307063f5a0 100644 --- a/integration-tests/api/__tests__/store/orders.js +++ b/integration-tests/api/__tests__/store/orders.js @@ -7,14 +7,14 @@ const { Product, ProductVariant, LineItem, + Payment, } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") - -const swapSeeder = require("../../helpers/swap-seeder") -const cartSeeder = require("../../helpers/cart-seeder") +const { simpleRegionFactory, simpleProductFactory } = require("../../factories") +const { MedusaError } = require("medusa-core-utils") jest.setTimeout(30000) @@ -25,7 +25,7 @@ describe("/store/carts", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) + medusaProcess = await setupServer({ cwd, verbose: false }) }) afterAll(async () => { @@ -147,4 +147,156 @@ describe("/store/carts", () => { expect(response.status).toEqual(404) }) }) + + describe("Cart Completion with INSUFFICIENT_INVENTORY", () => { + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("recovers from failed completion", async () => { + const api = useApi() + + const region = await simpleRegionFactory(dbConnection) + const product = await simpleProductFactory(dbConnection) + + const cartRes = await api + .post("/store/carts", { + region_id: region.id, + }) + .catch((err) => { + return err.response + }) + + const cartId = cartRes.data.cart.id + + await api.post(`/store/carts/${cartId}/line-items`, { + variant_id: product.variants[0].id, + quantity: 1, + }) + await api.post(`/store/carts/${cartId}`, { + email: "testmailer@medusajs.com", + }) + await api.post(`/store/carts/${cartId}/payment-sessions`) + + const manager = dbConnection.manager + await manager.update( + ProductVariant, + { id: product.variants[0].id }, + { + inventory_quantity: 0, + } + ) + + const responseFail = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseFail.status).toEqual(409) + expect(responseFail.data.type).toEqual("not_allowed") + expect(responseFail.data.code).toEqual( + MedusaError.Codes.INSUFFICIENT_INVENTORY + ) + + let payments = await manager.find(Payment, { cart_id: cartId }) + expect(payments).toHaveLength(1) + expect(payments).toContainEqual( + expect.objectContaining({ + canceled_at: expect.any(Date), + }) + ) + + await manager.update( + ProductVariant, + { id: product.variants[0].id }, + { + inventory_quantity: 1, + } + ) + + const responseSuccess = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseSuccess.status).toEqual(200) + expect(responseSuccess.data.type).toEqual("order") + + payments = await manager.find(Payment, { cart_id: cartId }) + expect(payments).toHaveLength(2) + expect(payments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + canceled_at: null, + }), + ]) + ) + }) + }) + + describe("Cart consecutive completion", () => { + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should fails on cart already completed", async () => { + const api = useApi() + const manager = dbConnection.manager + + const region = await simpleRegionFactory(dbConnection) + const product = await simpleProductFactory(dbConnection) + + const cartRes = await api + .post("/store/carts", { + region_id: region.id, + }) + .catch((err) => { + return err.response + }) + + const cartId = cartRes.data.cart.id + + await api.post(`/store/carts/${cartId}/line-items`, { + variant_id: product.variants[0].id, + quantity: 1, + }) + await api.post(`/store/carts/${cartId}`, { + email: "testmailer@medusajs.com", + }) + await api.post(`/store/carts/${cartId}/payment-sessions`) + + const responseSuccess = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseSuccess.status).toEqual(200) + expect(responseSuccess.data.type).toEqual("order") + + const payments = await manager.find(Payment, { cart_id: cartId }) + expect(payments).toHaveLength(1) + expect(payments).toContainEqual( + expect.objectContaining({ + canceled_at: null, + }) + ) + + const responseFail = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseFail.status).toEqual(409) + expect(responseFail.data.code).toEqual("cart_incompatible_state") + expect(responseFail.data.message).toEqual( + "Cart has already been completed" + ) + }) + }) }) diff --git a/packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts b/packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts new file mode 100644 index 0000000000..2ada304469 --- /dev/null +++ b/packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class multiPaymentCart1661345741249 implements MigrationInterface { + name = "multiPaymentCart1661345741249" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payment" DROP CONSTRAINT "REL_4665f17abc1e81dd58330e5854"` + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "UniquePaymentActive" ON "payment" ("cart_id") WHERE canceled_at IS NULL` + ) + await queryRunner.query( + `CREATE INDEX "IDX_aac4855eadda71aa1e4b6d7684" ON "payment" ("cart_id") WHERE canceled_at IS NOT NULL` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_aac4855eadda71aa1e4b6d7684"`) + await queryRunner.query(`DROP INDEX "UniquePaymentActive"`) + await queryRunner.query( + `ALTER TABLE "payment" ADD CONSTRAINT "REL_4665f17abc1e81dd58330e5854" UNIQUE ("cart_id")` + ) + } +} diff --git a/packages/medusa/src/models/payment.ts b/packages/medusa/src/models/payment.ts index 1fe31488db..456c86156d 100644 --- a/packages/medusa/src/models/payment.ts +++ b/packages/medusa/src/models/payment.ts @@ -16,6 +16,8 @@ import { Order } from "./order" import { Swap } from "./swap" import { generateEntityId } from "../utils/generate-entity-id" +@Index(["cart_id"], { where: "canceled_at IS NOT NULL" }) +@Index("UniquePaymentActive", ["cart_id"], { where: "canceled_at IS NULL", unique: true, }) @Entity() export class Payment extends BaseEntity { @Index() @@ -30,7 +32,7 @@ export class Payment extends BaseEntity { @Column({ nullable: true }) cart_id: string - @OneToOne(() => Cart) + @ManyToOne(() => Cart) @JoinColumn({ name: "cart_id" }) cart: Cart @@ -38,7 +40,10 @@ export class Payment extends BaseEntity { @Column({ nullable: true }) order_id: string - @ManyToOne(() => Order, (order) => order.payments) + @ManyToOne( + () => Order, + (order) => order.payments + ) @JoinColumn({ name: "order_id" }) order: Order From 4d4aa6889833bee212eacf099b8640a144144c96 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 25 Aug 2022 12:32:09 +0300 Subject: [PATCH 32/34] docs: Add upgrade guide for v1.3.6 (#2056) Added an upgrade guide for v1.3.6. Closes DOCS-143 Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/eighty-coats-joke.md | 5 +++ .../advanced/backend/upgrade-guides/1-3-6.md | 39 +++++++++++++++++++ www/docs/sidebars.js | 5 +++ 3 files changed, 49 insertions(+) create mode 100644 .changeset/eighty-coats-joke.md create mode 100644 docs/content/advanced/backend/upgrade-guides/1-3-6.md diff --git a/.changeset/eighty-coats-joke.md b/.changeset/eighty-coats-joke.md new file mode 100644 index 0000000000..1e45a5f47e --- /dev/null +++ b/.changeset/eighty-coats-joke.md @@ -0,0 +1,5 @@ +--- + +--- + +Add upgrade guide for v1.3.6 diff --git a/docs/content/advanced/backend/upgrade-guides/1-3-6.md b/docs/content/advanced/backend/upgrade-guides/1-3-6.md new file mode 100644 index 0000000000..240ba4cbc7 --- /dev/null +++ b/docs/content/advanced/backend/upgrade-guides/1-3-6.md @@ -0,0 +1,39 @@ +# v1.3.6 + +Following the addition of feature flags in version v1.3.3 and the addition of the Sales Channels API in v1.3.5, v1.3.6 introduces a data migration script that moves all products into the Default Sales Channel. + +:::note + +In version 1.3.6, Sales Channels are available but hidden behind feature flags. If you don’t have Sales Channels enabled, you don’t need to follow the steps detailed in this migration script. + +::: + +## Prerequisites + +Before performing the actions mentioned in this guide, you must set the following environment variables: + +```bash +TYPEORM_CONNECTION=postgres +TYPEORM_URL= +TYPEORM_LOGGING=true +TYPEORM_ENTITIES=./node_modules/@medusajs/medusa/dist/models/*.js +TYPEORM_MIGRATIONS=./node_modules/@medusajs/medusa/dist/migrations/*.js +``` + +These environment variables are used in the data migration scripts in this upgrade. Make sure to replace `` with your PostgreSQL database URL. + +## Sales Channels + +Sales Channels were introduced in v1.3.5 behind a feature flag. By enabling Sales Channels, developers and users can associate products and other entities with a specific Sales Channel. + +However, if you upgraded Medusa to v1.3.5 and enabled Sales Channels, you must add every product to at least one Sales Channel manually. Otherwise, products can’t be added to carts in different Sales Channels. + +v1.3.6 introduces a data migration script that automates this process for you by moving all your products into a default Sales Channel. This ensures that you can use the Sales Channels feature without it affecting the user experience in your store. + +### Actions Required + +If you’ve enabled Sales Channels, it’s essential to run the data migration script after upgrading your server and before starting your Medusa server: + +```bash +node ./node_modules/@medusajs/medusa/dist/scripts/sales-channels-migration.js +``` diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index ce56e24396..51cccdcc20 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -287,6 +287,11 @@ module.exports = { id: "advanced/backend/upgrade-guides/1-3-0", label: "v1.3.0" }, + { + type: "doc", + id: "advanced/backend/upgrade-guides/1-3-6", + label: "v1.3.6" + }, ] }, ] From d38a4fb927270117fad2be8326cb26677533ef30 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 25 Aug 2022 12:32:35 +0300 Subject: [PATCH 33/34] docs: Update Payment Provider documentation (#2057) --- .changeset/shiny-badgers-brake.md | 5 +++++ .../backend/payment/how-to-create-payment-provider.md | 4 ++-- docs/content/advanced/backend/payment/overview.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/shiny-badgers-brake.md diff --git a/.changeset/shiny-badgers-brake.md b/.changeset/shiny-badgers-brake.md new file mode 100644 index 0000000000..e756493a18 --- /dev/null +++ b/.changeset/shiny-badgers-brake.md @@ -0,0 +1,5 @@ +--- + +--- + +Update Payment Provider documentation to use new AbstractPaymentService interface diff --git a/docs/content/advanced/backend/payment/how-to-create-payment-provider.md b/docs/content/advanced/backend/payment/how-to-create-payment-provider.md index 7e93e5b6b2..f37166d87a 100644 --- a/docs/content/advanced/backend/payment/how-to-create-payment-provider.md +++ b/docs/content/advanced/backend/payment/how-to-create-payment-provider.md @@ -8,7 +8,7 @@ A Payment Provider is the payment method used to authorize, capture, and refund By default, Medusa has a [manual payment provider](https://github.com/medusajs/medusa/tree/master/packages/medusa-payment-manual) that has minimal implementation. It can be synonymous with a Cash on Delivery payment method. It allows store operators to manage the payment themselves but still keep track of its different stages on Medusa. -Adding a Payment Provider is as simple as creating a [service](../services/create-service.md) file in `src/services`. A Payment Provider is essentially a service that extends `PaymentService` from `medusa-interfaces`. +Adding a Payment Provider is as simple as creating a [service](../services/create-service.md) file in `src/services`. A Payment Provider is essentially a service that extends `AbstractPaymentService` from the core Medusa package `@medusajs/medusa`. Payment Provider Services must have a static property `identifier`. It is the name that will be used to install and refer to the Payment Provider in the Medusa server. @@ -58,7 +58,7 @@ export default MyPaymentService; Where `MyPaymentService` is the name of your Payment Provider service. For example, Stripe’s Payment Provider Service is called `StripeProviderService`. -Payment Providers must extend `PaymentService` from `medusa-interfaces`. +Payment Providers must extend `AbstractPaymentService` from the core Medusa package `@medusajs/medusa`. :::tip diff --git a/docs/content/advanced/backend/payment/overview.md b/docs/content/advanced/backend/payment/overview.md index b5650ae9e0..3ed89dd569 100644 --- a/docs/content/advanced/backend/payment/overview.md +++ b/docs/content/advanced/backend/payment/overview.md @@ -24,7 +24,7 @@ Payment Providers can also be related to a custom way of handling payment operat ### How Payment Provider is Created -A Payment Provider is essentially a Medusa [service](../services/create-service.md) with a unique identifier, and it extends the `PaymentService` provided by the `medusa-interfaces` package. It can be created as part of a [plugin](../plugins/overview.md), or it can be created just as a service file in your Medusa server. +A Payment Provider is essentially a Medusa [service](../services/create-service.md) with a unique identifier, and it extends the ``AbstractPaymentService` from the core Medusa package `@medusajs/medusa`. It can be created as part of a [plugin](../plugins/overview.md), or it can be created just as a service file in your Medusa server. As a developer, you will mainly work with the Payment Provider when integrating a payment method in Medusa. From 384c8efb8b85fa768d33c6b4ada43937cf1a8b3f Mon Sep 17 00:00:00 2001 From: olivermrbl Date: Thu, 25 Aug 2022 15:51:21 +0200 Subject: [PATCH 34/34] chore(release): Publish --- .changeset/cyan-years-complain.md | 5 -- .changeset/eighty-coats-joke.md | 5 -- .changeset/eighty-onions-share.md | 6 --- .changeset/fresh-snakes-judge.md | 5 -- .changeset/giant-jobs-battle.md | 5 -- .changeset/gorgeous-bears-hear.md | 5 -- .changeset/honest-garlics-help.md | 5 -- .changeset/khaki-spiders-hug.md | 5 -- .changeset/late-owls-pump.md | 6 --- .changeset/many-pumpkins-enjoy.md | 5 -- .changeset/new-icons-chew.md | 7 --- .changeset/proud-papayas-enjoy.md | 5 -- .changeset/shaggy-pots-explain.md | 5 -- .changeset/shaggy-steaks-change.md | 5 -- .changeset/shiny-badgers-brake.md | 5 -- .changeset/sixty-boats-flow.md | 5 -- .changeset/stale-mice-sip.md | 6 --- .changeset/thick-cows-guess.md | 5 -- .changeset/tiny-sheep-compare.md | 5 -- .changeset/tricky-suns-wink.md | 5 -- .changeset/wild-tables-compete.md | 5 -- packages/medusa-cli/package.json | 4 +- packages/medusa-file-minio/CHANGELOG.md | 7 +++ packages/medusa-file-minio/package.json | 4 +- packages/medusa-file-s3/CHANGELOG.md | 7 +++ packages/medusa-file-s3/package.json | 4 +- packages/medusa-file-spaces/CHANGELOG.md | 7 +++ packages/medusa-file-spaces/package.json | 4 +- .../medusa-fulfillment-manual/CHANGELOG.md | 9 ++++ .../medusa-fulfillment-manual/package.json | 4 +- packages/medusa-interfaces/CHANGELOG.md | 6 +++ packages/medusa-interfaces/package.json | 2 +- packages/medusa-js/CHANGELOG.md | 11 +++++ packages/medusa-js/package.json | 4 +- packages/medusa-payment-klarna/CHANGELOG.md | 7 +++ packages/medusa-payment-klarna/package.json | 4 +- packages/medusa-payment-paypal/CHANGELOG.md | 7 +++ packages/medusa-payment-paypal/package.json | 6 +-- packages/medusa-payment-stripe/CHANGELOG.md | 9 ++++ packages/medusa-payment-stripe/package.json | 4 +- packages/medusa-plugin-algolia/CHANGELOG.md | 7 +++ packages/medusa-plugin-algolia/package.json | 6 +-- .../medusa-plugin-meilisearch/CHANGELOG.md | 7 +++ .../medusa-plugin-meilisearch/package.json | 4 +- packages/medusa-react/CHANGELOG.md | 10 ++++ packages/medusa-react/package.json | 6 +-- packages/medusa-source-shopify/CHANGELOG.md | 7 +++ packages/medusa-source-shopify/package.json | 6 +-- packages/medusa-telemetry/CHANGELOG.md | 6 +++ packages/medusa-telemetry/package.json | 2 +- packages/medusa/CHANGELOG.md | 38 +++++++++++++++ packages/medusa/package.json | 6 +-- yarn.lock | 46 +++++++++---------- 53 files changed, 203 insertions(+), 168 deletions(-) delete mode 100644 .changeset/cyan-years-complain.md delete mode 100644 .changeset/eighty-coats-joke.md delete mode 100644 .changeset/eighty-onions-share.md delete mode 100644 .changeset/fresh-snakes-judge.md delete mode 100644 .changeset/giant-jobs-battle.md delete mode 100644 .changeset/gorgeous-bears-hear.md delete mode 100644 .changeset/honest-garlics-help.md delete mode 100644 .changeset/khaki-spiders-hug.md delete mode 100644 .changeset/late-owls-pump.md delete mode 100644 .changeset/many-pumpkins-enjoy.md delete mode 100644 .changeset/new-icons-chew.md delete mode 100644 .changeset/proud-papayas-enjoy.md delete mode 100644 .changeset/shaggy-pots-explain.md delete mode 100644 .changeset/shaggy-steaks-change.md delete mode 100644 .changeset/shiny-badgers-brake.md delete mode 100644 .changeset/sixty-boats-flow.md delete mode 100644 .changeset/stale-mice-sip.md delete mode 100644 .changeset/thick-cows-guess.md delete mode 100644 .changeset/tiny-sheep-compare.md delete mode 100644 .changeset/tricky-suns-wink.md delete mode 100644 .changeset/wild-tables-compete.md diff --git a/.changeset/cyan-years-complain.md b/.changeset/cyan-years-complain.md deleted file mode 100644 index 321c100b16..0000000000 --- a/.changeset/cyan-years-complain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Join tracking links to all fulfillments in admin/orders diff --git a/.changeset/eighty-coats-joke.md b/.changeset/eighty-coats-joke.md deleted file mode 100644 index 1e45a5f47e..0000000000 --- a/.changeset/eighty-coats-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- - ---- - -Add upgrade guide for v1.3.6 diff --git a/.changeset/eighty-onions-share.md b/.changeset/eighty-onions-share.md deleted file mode 100644 index 373a03fc56..0000000000 --- a/.changeset/eighty-onions-share.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"medusa-telemetry": patch -"@medusajs/medusa": patch ---- - -Adds enabled features flags to tracking event in `medusa-telemetry` diff --git a/.changeset/fresh-snakes-judge.md b/.changeset/fresh-snakes-judge.md deleted file mode 100644 index ab51fe994e..0000000000 --- a/.changeset/fresh-snakes-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Convert CollectionService to TypeScript diff --git a/.changeset/giant-jobs-battle.md b/.changeset/giant-jobs-battle.md deleted file mode 100644 index cdc6addaa9..0000000000 --- a/.changeset/giant-jobs-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa-js": patch ---- - -Add deleteSession endpoint diff --git a/.changeset/gorgeous-bears-hear.md b/.changeset/gorgeous-bears-hear.md deleted file mode 100644 index 9429ff0bca..0000000000 --- a/.changeset/gorgeous-bears-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"medusa-react": patch ---- - -Add Collection batch (remove, add) endpoints to medusa-react diff --git a/.changeset/honest-garlics-help.md b/.changeset/honest-garlics-help.md deleted file mode 100644 index 79df0a1707..0000000000 --- a/.changeset/honest-garlics-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- - ---- - -Use asymetric matcher for arrays diff --git a/.changeset/khaki-spiders-hug.md b/.changeset/khaki-spiders-hug.md deleted file mode 100644 index d6c2fc4767..0000000000 --- a/.changeset/khaki-spiders-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Allow filtering of customer orders diff --git a/.changeset/late-owls-pump.md b/.changeset/late-owls-pump.md deleted file mode 100644 index e9e2a1da11..0000000000 --- a/.changeset/late-owls-pump.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Convert IdempotencyKeyService to TypeScript -Add await to retrieve in lock method diff --git a/.changeset/many-pumpkins-enjoy.md b/.changeset/many-pumpkins-enjoy.md deleted file mode 100644 index 2f879986a4..0000000000 --- a/.changeset/many-pumpkins-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Fixes issue where failed cart completion attempts could not be retried without 500 error diff --git a/.changeset/new-icons-chew.md b/.changeset/new-icons-chew.md deleted file mode 100644 index 269b235443..0000000000 --- a/.changeset/new-icons-chew.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"medusa-fulfillment-manual": patch -"medusa-interfaces": patch -"@medusajs/medusa": patch ---- - -Convert FulfillmentService to TypeScript diff --git a/.changeset/proud-papayas-enjoy.md b/.changeset/proud-papayas-enjoy.md deleted file mode 100644 index 2db53f1019..0000000000 --- a/.changeset/proud-papayas-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Convert ShippingProfileService to TypeScript diff --git a/.changeset/shaggy-pots-explain.md b/.changeset/shaggy-pots-explain.md deleted file mode 100644 index cf041b5bba..0000000000 --- a/.changeset/shaggy-pots-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -add Mongolian native currency tugrug diff --git a/.changeset/shaggy-steaks-change.md b/.changeset/shaggy-steaks-change.md deleted file mode 100644 index 8d3342d1cc..0000000000 --- a/.changeset/shaggy-steaks-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Convert RegionService to TypeScript diff --git a/.changeset/shiny-badgers-brake.md b/.changeset/shiny-badgers-brake.md deleted file mode 100644 index e756493a18..0000000000 --- a/.changeset/shiny-badgers-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- - ---- - -Update Payment Provider documentation to use new AbstractPaymentService interface diff --git a/.changeset/sixty-boats-flow.md b/.changeset/sixty-boats-flow.md deleted file mode 100644 index b3a144bb7e..0000000000 --- a/.changeset/sixty-boats-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Convert OauthService to TypeScript diff --git a/.changeset/stale-mice-sip.md b/.changeset/stale-mice-sip.md deleted file mode 100644 index d6a4cb15f2..0000000000 --- a/.changeset/stale-mice-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"medusa-payment-stripe": patch -"@medusajs/medusa": patch ---- - -Add payment providers Przelewy24 and Blik through Stripe diff --git a/.changeset/thick-cows-guess.md b/.changeset/thick-cows-guess.md deleted file mode 100644 index 5f78a67373..0000000000 --- a/.changeset/thick-cows-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Convert SystemPaymentProvider to TypeScript diff --git a/.changeset/tiny-sheep-compare.md b/.changeset/tiny-sheep-compare.md deleted file mode 100644 index 0fc8f082ef..0000000000 --- a/.changeset/tiny-sheep-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Add new `isDefined` utility diff --git a/.changeset/tricky-suns-wink.md b/.changeset/tricky-suns-wink.md deleted file mode 100644 index ec435de4a2..0000000000 --- a/.changeset/tricky-suns-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa": patch ---- - -Use transactions in CartCompletionStrategy phases diff --git a/.changeset/wild-tables-compete.md b/.changeset/wild-tables-compete.md deleted file mode 100644 index 31de16fad0..0000000000 --- a/.changeset/wild-tables-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@medusajs/medusa-js": patch ---- - -Add batch endpoints (remove, add) for Collections to medusa-js diff --git a/packages/medusa-cli/package.json b/packages/medusa-cli/package.json index 261802bd21..8c6b9891e5 100644 --- a/packages/medusa-cli/package.json +++ b/packages/medusa-cli/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa-cli", - "version": "1.3.1", + "version": "1.3.2", "description": "Command Line interface for Medusa Commerce", "main": "dist/index.js", "bin": { @@ -52,7 +52,7 @@ "joi-objectid": "^3.0.1", "meant": "^1.0.1", "medusa-core-utils": "^0.1.27", - "medusa-telemetry": "^0.0.11", + "medusa-telemetry": "0.0.13", "netrc-parser": "^3.1.6", "open": "^8.0.6", "ora": "^5.4.1", diff --git a/packages/medusa-file-minio/CHANGELOG.md b/packages/medusa-file-minio/CHANGELOG.md index d7bb72bba7..a334210c07 100644 --- a/packages/medusa-file-minio/CHANGELOG.md +++ b/packages/medusa-file-minio/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.0.10 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.0.9 ### Patch Changes diff --git a/packages/medusa-file-minio/package.json b/packages/medusa-file-minio/package.json index 2d646e0b24..ef7c9e315f 100644 --- a/packages/medusa-file-minio/package.json +++ b/packages/medusa-file-minio/package.json @@ -1,6 +1,6 @@ { "name": "medusa-file-minio", - "version": "1.0.9", + "version": "1.0.10", "description": "MinIO server file connector for Medusa", "main": "index.js", "repository": { @@ -32,7 +32,7 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "@babel/plugin-transform-classes": "^7.16.0", diff --git a/packages/medusa-file-s3/CHANGELOG.md b/packages/medusa-file-s3/CHANGELOG.md index 4c8b3cea2b..ba9567d295 100644 --- a/packages/medusa-file-s3/CHANGELOG.md +++ b/packages/medusa-file-s3/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.1.5 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.1.4 ### Patch Changes diff --git a/packages/medusa-file-s3/package.json b/packages/medusa-file-s3/package.json index 3fea60210f..f197466040 100644 --- a/packages/medusa-file-s3/package.json +++ b/packages/medusa-file-s3/package.json @@ -1,6 +1,6 @@ { "name": "medusa-file-s3", - "version": "1.1.4", + "version": "1.1.5", "description": "AWS s3 file connector for Medusa", "main": "index.js", "repository": { @@ -33,7 +33,7 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "@babel/plugin-transform-classes": "^7.15.4", diff --git a/packages/medusa-file-spaces/CHANGELOG.md b/packages/medusa-file-spaces/CHANGELOG.md index 4c19008717..1716b77610 100644 --- a/packages/medusa-file-spaces/CHANGELOG.md +++ b/packages/medusa-file-spaces/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.2.5 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.2.4 ### Patch Changes diff --git a/packages/medusa-file-spaces/package.json b/packages/medusa-file-spaces/package.json index 76a556b83e..946cf89468 100644 --- a/packages/medusa-file-spaces/package.json +++ b/packages/medusa-file-spaces/package.json @@ -1,6 +1,6 @@ { "name": "medusa-file-spaces", - "version": "1.2.4", + "version": "1.2.5", "description": "Digital Ocean Spaces file connector for Medusa", "main": "index.js", "repository": { @@ -33,7 +33,7 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "@babel/plugin-transform-classes": "^7.9.5", diff --git a/packages/medusa-fulfillment-manual/CHANGELOG.md b/packages/medusa-fulfillment-manual/CHANGELOG.md index 484fa8c8d4..f4547004ac 100644 --- a/packages/medusa-fulfillment-manual/CHANGELOG.md +++ b/packages/medusa-fulfillment-manual/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## 1.1.32 + +### Patch Changes + +- [#1962](https://github.com/medusajs/medusa/pull/1962) [`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert FulfillmentService to TypeScript + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/medusa-fulfillment-manual/package.json b/packages/medusa-fulfillment-manual/package.json index a1e937329c..cd7f464fc4 100644 --- a/packages/medusa-fulfillment-manual/package.json +++ b/packages/medusa-fulfillment-manual/package.json @@ -1,6 +1,6 @@ { "name": "medusa-fulfillment-manual", - "version": "1.1.31", + "version": "1.1.32", "description": "A manual fulfillment provider for Medusa", "main": "index.js", "repository": { @@ -29,7 +29,7 @@ "watch": "babel -w src --out-dir ." }, "peerDependencies": { - "medusa-interfaces": "1.x" + "medusa-interfaces": "1.3.3" }, "dependencies": { "@babel/plugin-transform-classes": "^7.9.5", diff --git a/packages/medusa-interfaces/CHANGELOG.md b/packages/medusa-interfaces/CHANGELOG.md index 2e8cafa798..c678e00b6e 100644 --- a/packages/medusa-interfaces/CHANGELOG.md +++ b/packages/medusa-interfaces/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.3.3 + +### Patch Changes + +- [#1962](https://github.com/medusajs/medusa/pull/1962) [`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert FulfillmentService to TypeScript + ## 1.3.2 ### Patch Changes diff --git a/packages/medusa-interfaces/package.json b/packages/medusa-interfaces/package.json index 5c31886d47..edc9134fde 100644 --- a/packages/medusa-interfaces/package.json +++ b/packages/medusa-interfaces/package.json @@ -1,6 +1,6 @@ { "name": "medusa-interfaces", - "version": "1.3.2", + "version": "1.3.3", "description": "Core interfaces for Medusa", "main": "dist/index.js", "repository": { diff --git a/packages/medusa-js/CHANGELOG.md b/packages/medusa-js/CHANGELOG.md index 1121efe9a5..7066e3a6f6 100644 --- a/packages/medusa-js/CHANGELOG.md +++ b/packages/medusa-js/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## 1.2.6 + +### Patch Changes + +- [#1234](https://github.com/medusajs/medusa/pull/1234) [`8cbebef40`](https://github.com/medusajs/medusa/commit/8cbebef403a5ac5def1f95b2e591991cfa90b7fb) Thanks [@WalkingPizza](https://github.com/WalkingPizza)! - Add deleteSession endpoint + +* [#1958](https://github.com/medusajs/medusa/pull/1958) [`a88bf3c76`](https://github.com/medusajs/medusa/commit/a88bf3c76ea801d2b17227fb2eb8b8d8dbfe1262) Thanks [@richardwardza](https://github.com/richardwardza)! - Add batch endpoints (remove, add) for Collections to medusa-js + +* Updated dependencies [[`15a5b029a`](https://github.com/medusajs/medusa/commit/15a5b029ae3bd954481c558beeac87ace7ab945d), [`900260c5b`](https://github.com/medusajs/medusa/commit/900260c5b9df4f4f927db5bb6921e5e139ff269a), [`42ed20951`](https://github.com/medusajs/medusa/commit/42ed209518bf0278d1bef3c4c47d0ee21cae84c8), [`a54dc68db`](https://github.com/medusajs/medusa/commit/a54dc68db7a7d476cf4bf8d36c122c7f34629c90), [`aaebb38ea`](https://github.com/medusajs/medusa/commit/aaebb38eae883a225779b03556900ea813c991d2), [`9e0cb1212`](https://github.com/medusajs/medusa/commit/9e0cb1212023d7035165ddd269edab3efc7ebe29), [`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557), [`152934f8b`](https://github.com/medusajs/medusa/commit/152934f8b07cb3095788091df6823f9665fdf43d), [`8c4be3353`](https://github.com/medusajs/medusa/commit/8c4be3353630efd18759eb893666e44b1b49e2b7), [`bda83a84b`](https://github.com/medusajs/medusa/commit/bda83a84bc99a4741da2076f59071c177bc5534f), [`11fab121f`](https://github.com/medusajs/medusa/commit/11fab121f4c4b5ec3b6a3afccd4c44844bc5e3d9), [`40ae53567`](https://github.com/medusajs/medusa/commit/40ae53567a23ebe562e571fa22f1721eed174c82), [`80e02130b`](https://github.com/medusajs/medusa/commit/80e02130b4a444287920989654b607f07dd8d4f8), [`c31290c91`](https://github.com/medusajs/medusa/commit/c31290c911450a06d5e4da3dc5e4e3977071a6ea), [`4b663cca3`](https://github.com/medusajs/medusa/commit/4b663cca3acf43b0e02a1fb94b8d4f14913bfe45)]: + - @medusajs/medusa@1.3.6 + ## 1.2.5 ### Patch Changes diff --git a/packages/medusa-js/package.json b/packages/medusa-js/package.json index ca587ad160..f770ed606e 100644 --- a/packages/medusa-js/package.json +++ b/packages/medusa-js/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa-js", - "version": "1.2.5", + "version": "1.2.6", "description": "Client for Medusa Commerce Rest API", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -15,7 +15,7 @@ "author": "Oliver Juhl", "license": "MIT", "dependencies": { - "@medusajs/medusa": "^1.3.5", + "@medusajs/medusa": "^1.3.6", "axios": "^0.24.0", "form-data": "^4.0.0", "qs": "^6.10.3", diff --git a/packages/medusa-payment-klarna/CHANGELOG.md b/packages/medusa-payment-klarna/CHANGELOG.md index 1183110fd9..010ffab9b6 100644 --- a/packages/medusa-payment-klarna/CHANGELOG.md +++ b/packages/medusa-payment-klarna/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.3.3 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.3.2 ### Patch Changes diff --git a/packages/medusa-payment-klarna/package.json b/packages/medusa-payment-klarna/package.json index daf807abf7..6ec98db51e 100644 --- a/packages/medusa-payment-klarna/package.json +++ b/packages/medusa-payment-klarna/package.json @@ -1,6 +1,6 @@ { "name": "medusa-payment-klarna", - "version": "1.3.2", + "version": "1.3.3", "description": "Klarna Payment provider for Medusa Commerce", "main": "index.js", "repository": { @@ -33,7 +33,7 @@ "test": "jest" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "@babel/plugin-transform-classes": "^7.9.5", diff --git a/packages/medusa-payment-paypal/CHANGELOG.md b/packages/medusa-payment-paypal/CHANGELOG.md index 6e848fd3c9..0d6791cc1b 100644 --- a/packages/medusa-payment-paypal/CHANGELOG.md +++ b/packages/medusa-payment-paypal/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.2.5 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.2.4 ### Patch Changes diff --git a/packages/medusa-payment-paypal/package.json b/packages/medusa-payment-paypal/package.json index 56a2c4c14e..959c57244b 100644 --- a/packages/medusa-payment-paypal/package.json +++ b/packages/medusa-payment-paypal/package.json @@ -1,6 +1,6 @@ { "name": "medusa-payment-paypal", - "version": "1.2.4", + "version": "1.2.5", "description": "Paypal Payment provider for Meduas Commerce", "main": "index.js", "repository": { @@ -26,7 +26,7 @@ "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.2", - "medusa-interfaces": "^1.3.2", + "medusa-interfaces": "^1.3.3", "medusa-test-utils": "^1.1.37" }, "scripts": { @@ -36,7 +36,7 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "@paypal/checkout-server-sdk": "^1.0.2", diff --git a/packages/medusa-payment-stripe/CHANGELOG.md b/packages/medusa-payment-stripe/CHANGELOG.md index dfe354ec11..467c337354 100644 --- a/packages/medusa-payment-stripe/CHANGELOG.md +++ b/packages/medusa-payment-stripe/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## 1.1.43 + +### Patch Changes + +- [#1982](https://github.com/medusajs/medusa/pull/1982) [`40ae53567`](https://github.com/medusajs/medusa/commit/40ae53567a23ebe562e571fa22f1721eed174c82) Thanks [@chemicalkosek](https://github.com/chemicalkosek)! - Add payment providers Przelewy24 and Blik through Stripe + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.1.42 ### Patch Changes diff --git a/packages/medusa-payment-stripe/package.json b/packages/medusa-payment-stripe/package.json index 7850182a66..0ebc9a8c73 100644 --- a/packages/medusa-payment-stripe/package.json +++ b/packages/medusa-payment-stripe/package.json @@ -1,6 +1,6 @@ { "name": "medusa-payment-stripe", - "version": "1.1.42", + "version": "1.1.43", "description": "Stripe Payment provider for Meduas Commerce", "main": "index.js", "repository": { @@ -35,7 +35,7 @@ "test": "jest" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "body-parser": "^1.19.0", diff --git a/packages/medusa-plugin-algolia/CHANGELOG.md b/packages/medusa-plugin-algolia/CHANGELOG.md index d9ffad9b65..738d185b96 100644 --- a/packages/medusa-plugin-algolia/CHANGELOG.md +++ b/packages/medusa-plugin-algolia/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 0.2.4 ### Patch Changes diff --git a/packages/medusa-plugin-algolia/package.json b/packages/medusa-plugin-algolia/package.json index 6000212d1b..da39306eb9 100644 --- a/packages/medusa-plugin-algolia/package.json +++ b/packages/medusa-plugin-algolia/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-algolia", - "version": "0.2.4", + "version": "0.2.5", "description": "Search support for algolia", "main": "index.js", "repository": { @@ -17,7 +17,7 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "medusa-interfaces": "1.3.2", + "medusa-interfaces": "1.3.3", "typeorm": "0.x" }, "dependencies": { @@ -25,7 +25,7 @@ "body-parser": "^1.19.0", "lodash": "^4.17.21", "medusa-core-utils": "^1.1.31", - "medusa-interfaces": "^1.3.2" + "medusa-interfaces": "^1.3.3" }, "devDependencies": { "@babel/cli": "^7.7.5", diff --git a/packages/medusa-plugin-meilisearch/CHANGELOG.md b/packages/medusa-plugin-meilisearch/CHANGELOG.md index 644b0b4b6b..fe0cd58e1c 100644 --- a/packages/medusa-plugin-meilisearch/CHANGELOG.md +++ b/packages/medusa-plugin-meilisearch/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 0.2.4 ### Patch Changes diff --git a/packages/medusa-plugin-meilisearch/package.json b/packages/medusa-plugin-meilisearch/package.json index a3cf5bf591..97ef931bc7 100644 --- a/packages/medusa-plugin-meilisearch/package.json +++ b/packages/medusa-plugin-meilisearch/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-meilisearch", - "version": "0.2.4", + "version": "0.2.5", "description": "A starter for Medusa projects.", "main": "index.js", "repository": { @@ -17,7 +17,7 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "medusa-interfaces": "1.3.2" + "medusa-interfaces": "1.3.3" }, "dependencies": { "body-parser": "^1.19.0", diff --git a/packages/medusa-react/CHANGELOG.md b/packages/medusa-react/CHANGELOG.md index 90b41dcd1b..7532672f6d 100644 --- a/packages/medusa-react/CHANGELOG.md +++ b/packages/medusa-react/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 0.3.6 + +### Patch Changes + +- [#1959](https://github.com/medusajs/medusa/pull/1959) [`2a723dcd4`](https://github.com/medusajs/medusa/commit/2a723dcd4fb0074e7d34286c231ef248e907b1c4) Thanks [@richardwardza](https://github.com/richardwardza)! - Add Collection batch (remove, add) endpoints to medusa-react + +- Updated dependencies [[`15a5b029a`](https://github.com/medusajs/medusa/commit/15a5b029ae3bd954481c558beeac87ace7ab945d), [`900260c5b`](https://github.com/medusajs/medusa/commit/900260c5b9df4f4f927db5bb6921e5e139ff269a), [`42ed20951`](https://github.com/medusajs/medusa/commit/42ed209518bf0278d1bef3c4c47d0ee21cae84c8), [`8cbebef40`](https://github.com/medusajs/medusa/commit/8cbebef403a5ac5def1f95b2e591991cfa90b7fb), [`a54dc68db`](https://github.com/medusajs/medusa/commit/a54dc68db7a7d476cf4bf8d36c122c7f34629c90), [`aaebb38ea`](https://github.com/medusajs/medusa/commit/aaebb38eae883a225779b03556900ea813c991d2), [`9e0cb1212`](https://github.com/medusajs/medusa/commit/9e0cb1212023d7035165ddd269edab3efc7ebe29), [`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557), [`152934f8b`](https://github.com/medusajs/medusa/commit/152934f8b07cb3095788091df6823f9665fdf43d), [`8c4be3353`](https://github.com/medusajs/medusa/commit/8c4be3353630efd18759eb893666e44b1b49e2b7), [`bda83a84b`](https://github.com/medusajs/medusa/commit/bda83a84bc99a4741da2076f59071c177bc5534f), [`11fab121f`](https://github.com/medusajs/medusa/commit/11fab121f4c4b5ec3b6a3afccd4c44844bc5e3d9), [`40ae53567`](https://github.com/medusajs/medusa/commit/40ae53567a23ebe562e571fa22f1721eed174c82), [`80e02130b`](https://github.com/medusajs/medusa/commit/80e02130b4a444287920989654b607f07dd8d4f8), [`c31290c91`](https://github.com/medusajs/medusa/commit/c31290c911450a06d5e4da3dc5e4e3977071a6ea), [`4b663cca3`](https://github.com/medusajs/medusa/commit/4b663cca3acf43b0e02a1fb94b8d4f14913bfe45), [`a88bf3c76`](https://github.com/medusajs/medusa/commit/a88bf3c76ea801d2b17227fb2eb8b8d8dbfe1262)]: + - @medusajs/medusa@1.3.6 + - @medusajs/medusa-js@1.2.6 + ## 0.3.5 ### Patch Changes diff --git a/packages/medusa-react/package.json b/packages/medusa-react/package.json index 2208d12b41..a82952aa9f 100644 --- a/packages/medusa-react/package.json +++ b/packages/medusa-react/package.json @@ -1,5 +1,5 @@ { - "version": "0.3.5", + "version": "0.3.6", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -22,7 +22,7 @@ "build-storybook": "build-storybook" }, "peerDependencies": { - "@medusajs/medusa": "^1.3.5", + "@medusajs/medusa": "^1.3.6", "react": ">=16", "react-query": ">= 3.29.0" }, @@ -81,7 +81,7 @@ "tslib": "^2.3.1" }, "dependencies": { - "@medusajs/medusa-js": "^1.2.4", + "@medusajs/medusa-js": "^1.2.6", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "react-query": "^3.31.0" diff --git a/packages/medusa-source-shopify/CHANGELOG.md b/packages/medusa-source-shopify/CHANGELOG.md index 729af425d5..528e6dad1b 100644 --- a/packages/medusa-source-shopify/CHANGELOG.md +++ b/packages/medusa-source-shopify/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.2.3 + +### Patch Changes + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.2.2 ### Patch Changes diff --git a/packages/medusa-source-shopify/package.json b/packages/medusa-source-shopify/package.json index 0fc7f110ad..d6895197e2 100644 --- a/packages/medusa-source-shopify/package.json +++ b/packages/medusa-source-shopify/package.json @@ -1,6 +1,6 @@ { "name": "medusa-source-shopify", - "version": "1.2.2", + "version": "1.2.3", "description": "Source plugin that allows users to import products from a Shopify store", "main": "index.js", "repository": { @@ -17,7 +17,7 @@ "test": "jest" }, "peerDependencies": { - "medusa-interfaces": "1.3.2", + "medusa-interfaces": "1.3.3", "typeorm": "0.x" }, "dependencies": { @@ -29,7 +29,7 @@ "ioredis": "^4.27.9", "lodash": "^4.17.21", "medusa-core-utils": "^1.1.31", - "medusa-interfaces": "^1.3.2", + "medusa-interfaces": "^1.3.3", "medusa-test-utils": "^1.1.37" }, "devDependencies": { diff --git a/packages/medusa-telemetry/CHANGELOG.md b/packages/medusa-telemetry/CHANGELOG.md index 44aabe8738..4622e4e52f 100644 --- a/packages/medusa-telemetry/CHANGELOG.md +++ b/packages/medusa-telemetry/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.0.13 + +### Patch Changes + +- [#2017](https://github.com/medusajs/medusa/pull/2017) [`900260c5b`](https://github.com/medusajs/medusa/commit/900260c5b9df4f4f927db5bb6921e5e139ff269a) Thanks [@olivermrbl](https://github.com/olivermrbl)! - Adds enabled features flags to tracking event in `medusa-telemetry` + All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/medusa-telemetry/package.json b/packages/medusa-telemetry/package.json index 984faf8783..d7884cfe47 100644 --- a/packages/medusa-telemetry/package.json +++ b/packages/medusa-telemetry/package.json @@ -1,6 +1,6 @@ { "name": "medusa-telemetry", - "version": "0.0.11", + "version": "0.0.13", "description": "Telemetry for Medusa", "main": "dist/index.js", "repository": { diff --git a/packages/medusa/CHANGELOG.md b/packages/medusa/CHANGELOG.md index 7fcc8dbf04..af60015a61 100644 --- a/packages/medusa/CHANGELOG.md +++ b/packages/medusa/CHANGELOG.md @@ -1,5 +1,43 @@ # Change Log +## 1.3.6 + +### Patch Changes + +- [#2045](https://github.com/medusajs/medusa/pull/2045) [`15a5b029a`](https://github.com/medusajs/medusa/commit/15a5b029ae3bd954481c558beeac87ace7ab945d) Thanks [@srindom](https://github.com/srindom)! - Join tracking links to all fulfillments in admin/orders + +* [#2017](https://github.com/medusajs/medusa/pull/2017) [`900260c5b`](https://github.com/medusajs/medusa/commit/900260c5b9df4f4f927db5bb6921e5e139ff269a) Thanks [@olivermrbl](https://github.com/olivermrbl)! - Adds enabled features flags to tracking event in `medusa-telemetry` + +- [#1976](https://github.com/medusajs/medusa/pull/1976) [`42ed20951`](https://github.com/medusajs/medusa/commit/42ed209518bf0278d1bef3c4c47d0ee21cae84c8) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert CollectionService to TypeScript + +* [#975](https://github.com/medusajs/medusa/pull/975) [`a54dc68db`](https://github.com/medusajs/medusa/commit/a54dc68db7a7d476cf4bf8d36c122c7f34629c90) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Allow filtering of customer orders + +- [#1995](https://github.com/medusajs/medusa/pull/1995) [`aaebb38ea`](https://github.com/medusajs/medusa/commit/aaebb38eae883a225779b03556900ea813c991d2) Thanks [@adrien2p](https://github.com/adrien2p)! - Convert IdempotencyKeyService to TypeScript + Add await to retrieve in lock method + +* [#1854](https://github.com/medusajs/medusa/pull/1854) [`9e0cb1212`](https://github.com/medusajs/medusa/commit/9e0cb1212023d7035165ddd269edab3efc7ebe29) Thanks [@srindom](https://github.com/srindom)! - Fixes issue where failed cart completion attempts could not be retried without 500 error + +- [#1962](https://github.com/medusajs/medusa/pull/1962) [`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert FulfillmentService to TypeScript + +* [#1963](https://github.com/medusajs/medusa/pull/1963) [`152934f8b`](https://github.com/medusajs/medusa/commit/152934f8b07cb3095788091df6823f9665fdf43d) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert ShippingProfileService to TypeScript + +- [#2067](https://github.com/medusajs/medusa/pull/2067) [`8c4be3353`](https://github.com/medusajs/medusa/commit/8c4be3353630efd18759eb893666e44b1b49e2b7) Thanks [@endigo](https://github.com/endigo)! - add Mongolian native currency tugrug + +* [#1914](https://github.com/medusajs/medusa/pull/1914) [`bda83a84b`](https://github.com/medusajs/medusa/commit/bda83a84bc99a4741da2076f59071c177bc5534f) Thanks [@fPolic](https://github.com/fPolic)! - Convert RegionService to TypeScript + +- [#1983](https://github.com/medusajs/medusa/pull/1983) [`11fab121f`](https://github.com/medusajs/medusa/commit/11fab121f4c4b5ec3b6a3afccd4c44844bc5e3d9) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert OauthService to TypeScript + +* [#1982](https://github.com/medusajs/medusa/pull/1982) [`40ae53567`](https://github.com/medusajs/medusa/commit/40ae53567a23ebe562e571fa22f1721eed174c82) Thanks [@chemicalkosek](https://github.com/chemicalkosek)! - Add payment providers Przelewy24 and Blik through Stripe + +- [#1988](https://github.com/medusajs/medusa/pull/1988) [`80e02130b`](https://github.com/medusajs/medusa/commit/80e02130b4a444287920989654b607f07dd8d4f8) Thanks [@pKorsholm](https://github.com/pKorsholm)! - Convert SystemPaymentProvider to TypeScript + +* [#2024](https://github.com/medusajs/medusa/pull/2024) [`c31290c91`](https://github.com/medusajs/medusa/commit/c31290c911450a06d5e4da3dc5e4e3977071a6ea) Thanks [@adrien2p](https://github.com/adrien2p)! - Add new `isDefined` utility + +- [#1968](https://github.com/medusajs/medusa/pull/1968) [`4b663cca3`](https://github.com/medusajs/medusa/commit/4b663cca3acf43b0e02a1fb94b8d4f14913bfe45) Thanks [@adrien2p](https://github.com/adrien2p)! - Use transactions in CartCompletionStrategy phases + +- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]: + - medusa-interfaces@1.3.3 + ## 1.3.5 ### Patch Changes diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 6aa2288241..76ac91fefe 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa", - "version": "1.3.5", + "version": "1.3.6", "description": "E-commerce for JAMstack", "main": "dist/index.js", "bin": "./cli.js", @@ -26,7 +26,7 @@ "cross-env": "^5.2.1", "eslint": "^7.32.0", "jest": "^25.5.2", - "medusa-interfaces": "^1.3.2", + "medusa-interfaces": "^1.3.3", "nodemon": "^2.0.1", "prettier": "^1.19.1", "sqlite3": "^5.0.2", @@ -43,7 +43,7 @@ "test:unit": "jest" }, "peerDependencies": { - "medusa-interfaces": "1.3.2", + "medusa-interfaces": "1.3.3", "typeorm": "0.2.x" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index 073b73c3a6..cba000d4e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5168,7 +5168,7 @@ __metadata: joi-objectid: ^3.0.1 meant: ^1.0.1 medusa-core-utils: ^0.1.27 - medusa-telemetry: ^0.0.11 + medusa-telemetry: 0.0.13 netrc-parser: ^3.1.6 open: ^8.0.6 ora: ^5.4.1 @@ -5187,11 +5187,11 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/medusa-js@^1.2.4, @medusajs/medusa-js@workspace:packages/medusa-js": +"@medusajs/medusa-js@^1.2.6, @medusajs/medusa-js@workspace:packages/medusa-js": version: 0.0.0-use.local resolution: "@medusajs/medusa-js@workspace:packages/medusa-js" dependencies: - "@medusajs/medusa": ^1.3.5 + "@medusajs/medusa": ^1.3.6 "@types/jest": ^26.0.19 axios: ^0.24.0 cross-env: ^7.0.3 @@ -5205,7 +5205,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/medusa@^1.3.3, @medusajs/medusa@^1.3.5, @medusajs/medusa@workspace:packages/medusa": +"@medusajs/medusa@^1.3.3, @medusajs/medusa@^1.3.6, @medusajs/medusa@workspace:packages/medusa": version: 0.0.0-use.local resolution: "@medusajs/medusa@workspace:packages/medusa" dependencies: @@ -5245,7 +5245,7 @@ __metadata: joi-objectid: ^3.0.1 jsonwebtoken: ^8.5.1 medusa-core-utils: ^1.1.31 - medusa-interfaces: ^1.3.2 + medusa-interfaces: ^1.3.3 medusa-test-utils: ^1.1.37 morgan: ^1.9.1 multer: ^1.4.2 @@ -5272,7 +5272,7 @@ __metadata: uuid: ^8.3.1 winston: ^3.2.1 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 typeorm: 0.2.x bin: medusa: ./cli.js @@ -25129,7 +25129,7 @@ __metadata: medusa-core-utils: ^1.1.31 medusa-test-utils: ^1.1.37 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25157,7 +25157,7 @@ __metadata: medusa-core-utils: ^1.1.31 medusa-test-utils: ^1.1.37 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25186,7 +25186,7 @@ __metadata: medusa-test-utils: ^1.1.37 stripe: ^8.50.0 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25209,7 +25209,7 @@ __metadata: jest: ^25.5.2 medusa-core-utils: ^1.1.31 peerDependencies: - medusa-interfaces: 1.x + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25239,7 +25239,7 @@ __metadata: languageName: unknown linkType: soft -"medusa-interfaces@^1.3.1, medusa-interfaces@^1.3.2, medusa-interfaces@workspace:packages/medusa-interfaces": +"medusa-interfaces@^1.3.1, medusa-interfaces@^1.3.3, medusa-interfaces@workspace:packages/medusa-interfaces": version: 0.0.0-use.local resolution: "medusa-interfaces@workspace:packages/medusa-interfaces" dependencies: @@ -25321,7 +25321,7 @@ __metadata: medusa-core-utils: ^1.1.31 medusa-test-utils: ^1.1.37 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25372,10 +25372,10 @@ __metadata: express: ^4.17.1 jest: ^25.5.2 medusa-core-utils: ^1.1.31 - medusa-interfaces: ^1.3.2 + medusa-interfaces: ^1.3.3 medusa-test-utils: ^1.1.37 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25404,7 +25404,7 @@ __metadata: medusa-test-utils: ^1.1.37 stripe: ^8.50.0 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25429,9 +25429,9 @@ __metadata: jest: ^25.5.2 lodash: ^4.17.21 medusa-core-utils: ^1.1.31 - medusa-interfaces: ^1.3.2 + medusa-interfaces: ^1.3.3 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 typeorm: 0.x languageName: unknown linkType: soft @@ -25625,7 +25625,7 @@ __metadata: medusa-core-utils: ^1.1.31 meilisearch: ^0.24.0 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 languageName: unknown linkType: soft @@ -25797,7 +25797,7 @@ __metadata: resolution: "medusa-react@workspace:packages/medusa-react" dependencies: "@babel/core": ^7.16.0 - "@medusajs/medusa-js": ^1.2.4 + "@medusajs/medusa-js": ^1.2.6 "@size-limit/preset-small-lib": ^6.0.4 "@storybook/addon-contexts": ^5.3.21 "@storybook/addon-essentials": ^6.3.12 @@ -25828,7 +25828,7 @@ __metadata: ts-jest: ^27.1.4 tslib: ^2.3.1 peerDependencies: - "@medusajs/medusa": ^1.3.5 + "@medusajs/medusa": ^1.3.6 react: ">=16" react-query: ">= 3.29.0" languageName: unknown @@ -25859,15 +25859,15 @@ __metadata: jest: ^26.6.3 lodash: ^4.17.21 medusa-core-utils: ^1.1.31 - medusa-interfaces: ^1.3.2 + medusa-interfaces: ^1.3.3 medusa-test-utils: ^1.1.37 peerDependencies: - medusa-interfaces: 1.3.2 + medusa-interfaces: 1.3.3 typeorm: 0.x languageName: unknown linkType: soft -"medusa-telemetry@^0.0.11, medusa-telemetry@workspace:packages/medusa-telemetry": +"medusa-telemetry@0.0.13, medusa-telemetry@workspace:packages/medusa-telemetry": version: 0.0.0-use.local resolution: "medusa-telemetry@workspace:packages/medusa-telemetry" dependencies: