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 77947f8a09..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. @@ -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 { } @@ -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. 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/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index 3b8d3f10f5..903df5ce96 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", + }), + ]) + ) }) }) @@ -609,16 +618,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] @@ -1468,6 +1480,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 () => { diff --git a/integration-tests/api/__tests__/admin/order.js b/integration-tests/api/__tests__/admin/order.js index 32b7af183e..52ae78a2f5 100644 --- a/integration-tests/api/__tests__/admin/order.js +++ b/integration-tests/api/__tests__/admin/order.js @@ -653,11 +653,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 () => { @@ -807,19 +810,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 () => { @@ -872,27 +877,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 () => { @@ -1288,14 +1300,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 () => { @@ -1385,28 +1400,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 () => { @@ -1424,14 +1442,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 () => { @@ -1467,12 +1488,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 () => { @@ -1513,27 +1537,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 () => { @@ -1565,27 +1592,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 () => { @@ -1617,27 +1647,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([ @@ -1826,11 +1859,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 fb8479ef89..e065e6222e 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -353,10 +353,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" }), + ]) + ) }) }) @@ -1137,52 +1140,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 () => { @@ -1200,9 +1206,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 () => { @@ -1220,12 +1229,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 09d78ea7ca..0597744932 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -160,16 +160,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( @@ -199,16 +202,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( @@ -232,11 +238,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 () => { @@ -317,11 +326,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 () => { @@ -343,16 +355,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( @@ -426,13 +441,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 9ee9b923e4..f6fd86e5bc 100644 --- a/integration-tests/api/__tests__/admin/region.js +++ b/integration-tests/api/__tests__/admin/region.js @@ -157,17 +157,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) }) @@ -184,14 +187,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 d9eac8270e..84ca8519e5 100644 --- a/integration-tests/api/__tests__/admin/return-reason.js +++ b/integration-tests/api/__tests__/admin/return-reason.js @@ -350,20 +350,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 () => { @@ -396,11 +399,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 0cc5d3f6bd..50f3fffdfa 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -783,11 +783,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__/admin/swaps.js b/integration-tests/api/__tests__/admin/swaps.js index ecda8da1af..2fa385b782 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", () => { @@ -136,4 +147,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__/returns/index.js b/integration-tests/api/__tests__/returns/index.js index 5366269a90..ab95750efc 100644 --- a/integration-tests/api/__tests__/returns/index.js +++ b/integration-tests/api/__tests__/returns/index.js @@ -68,13 +68,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 () => { @@ -109,13 +112,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 () => { @@ -159,20 +165,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 () => { @@ -210,13 +222,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 0a8648f0d1..4d33520978 100644 --- a/integration-tests/api/__tests__/store/cart.js +++ b/integration-tests/api/__tests__/store/cart.js @@ -151,17 +151,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) @@ -226,15 +229,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 () => { @@ -251,21 +257,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 () => { @@ -282,21 +291,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 () => { @@ -313,21 +325,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 () => { @@ -344,21 +359,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 () => { @@ -375,14 +393,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 () => { @@ -413,14 +434,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 () => { @@ -437,14 +461,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 () => { @@ -461,14 +488,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", () => { @@ -665,17 +695,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", + }), + ], + }), + ]) + ) }) }) }) @@ -739,21 +771,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 () => { @@ -779,21 +814,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 () => { @@ -819,21 +857,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 () => { @@ -859,21 +900,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", + }), + ], + }), + ]) + ) }) }) @@ -1564,6 +1608,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/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/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/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index 5f6a627948..ef053b75c2 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -50,12 +50,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( @@ -76,12 +79,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( @@ -101,12 +107,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 () => { @@ -139,12 +147,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( @@ -167,12 +178,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( @@ -196,27 +210,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( @@ -246,77 +262,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 477093d793..ee8e66699f 100644 --- a/integration-tests/api/__tests__/store/return-reason.js +++ b/integration-tests/api/__tests__/store/return-reason.js @@ -108,22 +108,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/manual-taxes.js b/integration-tests/api/__tests__/taxes/manual-taxes.js index dcf33767f9..c787159626 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/__tests__/taxes/orders.js b/integration-tests/api/__tests__/taxes/orders.js index bde86a119b..580667dd00 100644 --- a/integration-tests/api/__tests__/taxes/orders.js +++ b/integration-tests/api/__tests__/taxes/orders.js @@ -286,16 +286,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 () => { @@ -377,15 +383,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 ea6d1e10b9..b262dd93fd 100644 --- a/integration-tests/api/__tests__/taxes/shipping-options.js +++ b/integration-tests/api/__tests__/taxes/shipping-options.js @@ -64,13 +64,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 () => { @@ -93,12 +96,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 52c715daae..97b9343ee8 100644 --- a/integration-tests/api/__tests__/totals/orders.js +++ b/integration-tests/api/__tests__/totals/orders.js @@ -89,13 +89,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) @@ -151,13 +154,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) 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 3427a8623d..3f64739050 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/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/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/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-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-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-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-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/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-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-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-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-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 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. 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-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 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) } } } 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/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/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-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 + }) + ) + }) +}) 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-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/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/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/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/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/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/__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/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/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 = [ 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..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,12 +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 @@ -140,7 +142,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) { @@ -196,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"] }) @@ -205,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 @@ -277,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/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 726bfc65b2..adbafc4518 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 @@ -169,7 +170,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/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/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/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/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/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 5d0e34e6f9..5121835a6c 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} @@ -93,11 +94,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, @@ -105,7 +106,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/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/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/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/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/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/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/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/api/routes/store/returns/create-return.ts b/packages/medusa/src/api/routes/store/returns/create-return.ts index 600c802f76..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") @@ -108,7 +105,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) { @@ -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/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/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/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/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/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/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/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 new file mode 100644 index 0000000000..216052d230 --- /dev/null +++ b/packages/medusa/src/interfaces/payment-service.ts @@ -0,0 +1,114 @@ +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 + 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/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/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/feature-flags/index.ts b/packages/medusa/src/loaders/feature-flags/index.ts index 368365da26..60874b43c4 100644 --- a/packages/medusa/src/loaders/feature-flags/index.ts +++ b/packages/medusa/src/loaders/feature-flags/index.ts @@ -1,9 +1,11 @@ -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" +import { isDefined } from "../../utils" const isTruthy = (val: string | boolean | undefined): boolean => { if (typeof val === "string") { @@ -27,40 +29,34 @@ 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 (isDefined(process.env[flagSettings.env_key])) { + from = "environment" + flagConfig[flagSettings.key] = isTruthy(process.env[flagSettings.env_key]) + } else if (isDefined(projectConfigFlags[flagSettings.key])) { + 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]) { + trackFeatureFlag(flagSettings.key) } } 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 } diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 182c287f78..aa16d7c041 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -10,15 +10,16 @@ import { FileService, FulfillmentService, OauthService, - PaymentService, } from "medusa-interfaces" import path from "path" import { EntitySchema } from "typeorm" import { AbstractTaxService, isBatchJobStrategy, + isCartCompletionStrategy, isFileService, isNotificationService, + isPaymentService, isPriceSelectionStrategy, isSearchService, isTaxCalculationStrategy, @@ -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", @@ -349,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/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/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/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/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-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..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 @@ -63,10 +68,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/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/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 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__/order.js b/packages/medusa/src/services/__tests__/order.js index 77d4a9d86b..83e8d837cd 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1,10 +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" describe("OrderService", () => { const totalsService = { - withTransaction: function () { + withTransaction: function() { return this }, getLineItemRefund: () => {}, @@ -39,7 +40,7 @@ describe("OrderService", () => { const eventBusService = { emit: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -55,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 }, } @@ -80,7 +81,7 @@ describe("OrderService", () => { cancelPayment: jest.fn().mockImplementation((payment) => { return Promise.resolve({ ...payment, status: "cancelled" }) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -119,7 +120,7 @@ describe("OrderService", () => { total: 100, }) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -209,6 +210,8 @@ describe("OrderService", () => { "discounts.rule", "gift_cards", "shipping_methods", + "items", + "items.adjustments", ], }) @@ -520,6 +523,7 @@ describe("OrderService", () => { manager: MockManager, orderRepository: orderRepo, eventBusService, + lineItemService: LineItemServiceMock, }) beforeEach(async () => { @@ -613,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 }, } @@ -717,7 +721,7 @@ describe("OrderService", () => { ? Promise.reject() : Promise.resolve({ ...p, captured_at: "notnull" }) ), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -822,7 +826,7 @@ describe("OrderService", () => { const lineItemService = { update: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -835,7 +839,7 @@ describe("OrderService", () => { }, ]) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1002,7 +1006,7 @@ describe("OrderService", () => { }) } }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1071,7 +1075,7 @@ describe("OrderService", () => { .mockImplementation((p) => p.id === "payment_fail" ? Promise.reject() : Promise.resolve() ), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1212,7 +1216,7 @@ describe("OrderService", () => { .fn() .mockImplementation(() => Promise.resolve({})), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1346,7 +1350,7 @@ describe("OrderService", () => { const lineItemService = { update: jest.fn(), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1369,7 +1373,7 @@ describe("OrderService", () => { ], }) }), - withTransaction: function () { + withTransaction: function() { return this }, } @@ -1396,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 } @@ -1488,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/__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__/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/__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/__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/__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/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 bc8ee5d24d..826b3e735b 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -3,16 +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 { - DiscountRuleType, Address, Cart, - CustomShippingOption, Customer, + CustomShippingOption, Discount, + DiscountRuleType, LineItem, - ShippingMethod, + PaymentSession, SalesChannel, + ShippingMethod, } from "../models" import { AddressRepository } from "../repositories/address" import { CartRepository } from "../repositories/cart" @@ -26,12 +28,14 @@ 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 { 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" @@ -40,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 @@ -82,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", @@ -392,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" @@ -402,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" @@ -422,10 +426,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] } @@ -447,7 +448,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) @@ -857,7 +858,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 @@ -865,14 +866,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 @@ -901,7 +899,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) @@ -909,7 +907,7 @@ class CartService extends TransactionBaseService { } } - if (typeof data.discounts !== "undefined") { + if (isDefined(data.discounts)) { const previousDiscounts = [...cart.discounts] cart.discounts.length = 0 @@ -1310,7 +1308,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, { @@ -1375,7 +1376,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) { @@ -1385,14 +1386,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_ @@ -1738,14 +1739,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) } @@ -1753,13 +1757,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-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 82340a5c26..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" @@ -25,7 +26,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" @@ -49,10 +50,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", @@ -150,32 +148,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 +181,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 +231,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 || @@ -336,25 +333,32 @@ export default class ClaimService extends TransactionBaseService< } let newItems: LineItem[] = [] - if (typeof additional_items !== "undefined") { + 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,57 +382,58 @@ 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) { 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, }) @@ -584,14 +589,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 +713,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 +723,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/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-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..fc5c1b6b7d 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" @@ -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 @@ -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/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 cffb387a0b..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", @@ -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/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-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/fulfillment.ts b/packages/medusa/src/services/fulfillment.ts index 0756f335c7..ea882f94da 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" @@ -32,7 +32,7 @@ type InjectedDependencies = { /** * Handles Fulfillments */ -class FulfillmentService extends TransactionBaseService { +class FulfillmentService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined @@ -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, }) } @@ -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..d2c626bf59 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 = { @@ -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 @@ -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/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..12e4b8c37a --- /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/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/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/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.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..5ca811aaab --- /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/services/order.ts b/packages/medusa/src/services/order.ts index c68bb4a4f9..34d63507f1 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, @@ -30,17 +11,37 @@ import { Order, OrderStatus, Payment, + PaymentSession, PaymentStatus, Return, 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 @@ -63,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", @@ -181,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 = { @@ -478,10 +484,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", @@ -491,6 +497,8 @@ class OrderService extends TransactionBaseService { "discounts.rule", "gift_cards", "shipping_methods", + "items", + "items.adjustments", ], }) @@ -505,7 +513,7 @@ class OrderService extends TransactionBaseService { for (const item of cart.items) { try { - await inventoryService.confirmInventory( + await inventoryServiceTx.confirmInventory( item.variant_id, item.quantity ) @@ -515,7 +523,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 } } @@ -531,7 +539,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 +550,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, @@ -619,13 +625,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_ @@ -635,7 +644,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 }) @@ -698,6 +707,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) @@ -707,7 +718,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 { @@ -840,6 +851,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) { @@ -847,9 +861,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) } @@ -928,9 +940,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, }) @@ -1005,16 +1018,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 @@ -1052,11 +1064,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_ @@ -1252,14 +1266,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/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..075e1f748b --- /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/price-list.ts b/packages/medusa/src/services/price-list.ts index bac06de9c3..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 @@ -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/pricing.ts b/packages/medusa/src/services/pricing.ts index 5cc9a50746..6af05e28ef 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 @@ -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-collection.js b/packages/medusa/src/services/product-collection.ts similarity index 52% rename from packages/medusa/src/services/product-collection.js rename to packages/medusa/src/services/product-collection.ts index 53422a5e2f..51a0422e61 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/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 9715c15c45..c96827ab4d 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. @@ -359,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 */ @@ -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..a5bf4163fc 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" @@ -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 @@ -393,7 +390,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 +461,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 +510,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) @@ -658,10 +655,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/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/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/return.js b/packages/medusa/src/services/return.ts similarity index 60% rename from packages/medusa/src/services/return.js rename to packages/medusa/src/services/return.ts index 81696b2472..30308c9e8a 100644 --- a/packages/medusa/src/services/return.js +++ b/packages/medusa/src/services/return.ts @@ -1,11 +1,66 @@ +import { isDefined } from "class-validator" import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" +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, @@ -18,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 @@ -109,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" @@ -144,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) }) } @@ -155,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, @@ -169,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" @@ -181,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, @@ -204,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) { @@ -222,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) @@ -245,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, }) @@ -265,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") { @@ -282,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)) { @@ -290,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) }) } @@ -299,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 || @@ -350,19 +395,19 @@ 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 - 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 - if (toRefund > refundable) { + if (toRefund! > refundable) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Cannot refund more than the original payment" @@ -370,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 @@ -378,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( @@ -403,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( @@ -438,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) } } @@ -447,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", @@ -469,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), }, @@ -481,7 +527,7 @@ class ReturnService extends BaseService { return { ...item, item: found, - } + } as ReturnItem }) if (returnOrder.shipping_data) { @@ -495,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) }) } @@ -514,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" @@ -568,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) => { @@ -603,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 = { @@ -624,22 +666,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/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 358d3e65a4..83dee51501 100644 --- a/packages/medusa/src/services/shipping-option.ts +++ b/packages/medusa/src/services/shipping-option.ts @@ -19,14 +19,14 @@ 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" /** * 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 @@ -252,7 +252,7 @@ class ShippingOptionService extends TransactionBaseService, config: CreateShippingMethodDto ): Promise { return await this.atomicPhase_(async (manager) => { @@ -262,7 +262,7 @@ class ShippingOptionService extends TransactionBaseService, cart: Cart | Order | undefined ): Promise { if (option.price_type === "calculated") { 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..2eb599207f --- /dev/null +++ b/packages/medusa/src/services/shipping-profile.ts @@ -0,0 +1,482 @@ +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) { + const productServiceTx = this.productService_.withTransaction(manager) + for (const pId of products) { + await productServiceTx.update(pId, { + profile_id: profile.id, + }) + } + } + + if (shipping_options) { + const shippingOptionServiceTx = + this.shippingOptionService_.withTransaction(manager) + for (const oId of shipping_options) { + await shippingOptionServiceTx.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/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/swap.js b/packages/medusa/src/services/swap.js index 8d54d95622..657cdc1a58 100644 --- a/packages/medusa/src/services/swap.js +++ b/packages/medusa/src/services/swap.js @@ -1,5 +1,6 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { isDefined } from "../utils" /** * Handles swaps @@ -117,7 +118,7 @@ class SwapService extends BaseService { let cartSelects = null let cartRelations = null - if (typeof relations !== "undefined" && relations.includes("cart")) { + if (isDefined(relations) && relations.includes("cart")) { const [swapRelations, cartRels] = relations.reduce( (acc, next) => { 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.")) { @@ -183,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) @@ -599,24 +603,30 @@ class SwapService extends BaseService { }, }) + const customShippingOptionServiceTx = this.customShippingOptionService_.withTransaction( + manager + ) for (const customShippingOption of customShippingOptions) { - await this.customShippingOptionService_ - .withTransaction(manager) - .create({ - cart_id: cart.id, - shipping_option_id: customShippingOption.option_id, - price: customShippingOption.price, - }) + await customShippingOptionServiceTx.create({ + cart_id: cart.id, + shipping_option_id: customShippingOption.option_id, + price: customShippingOption.price, + }) } + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + const lineItemAdjustmentServiceTx = this.lineItemAdjustmentService_.withTransaction( + manager + ) for (const item of swap.additional_items) { - await this.lineItemService_.withTransaction(manager).update(item.id, { + await lineItemServiceTx.update(item.id, { cart_id: cart.id, }) // we generate adjustments in case the cart has any discounts that should be applied to the additional items - await this.lineItemAdjustmentService_ - .withTransaction(manager) - .createAdjustmentForLineItem(cart, item) + await lineItemAdjustmentServiceTx.createAdjustmentForLineItem( + cart, + item + ) } // If the swap has a return shipping method the price has to be added to @@ -686,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 @@ -694,20 +709,25 @@ 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 cartServiceTx = this.cartService_.withTransaction(manager) + for (const item of items) { try { - await this.inventoryService_ - .withTransaction(manager) - .confirmInventory(item.variant_id, item.quantity) + await inventoryServiceTx.confirmInventory( + item.variant_id, + item.quantity + ) } catch (err) { if (payment) { - await this.paymentProviderService_ - .withTransaction(manager) - .cancelPayment(payment) + await paymentProviderServiceTx.cancelPayment(payment) } - await this.cartService_ - .withTransaction(manager) - .update(cart.id, { payment_authorized_at: null }) + await cartServiceTx.update(cart.id, { payment_authorized_at: null }) throw err } } @@ -742,10 +762,15 @@ class SwapService extends BaseService { order_id: swap.order_id, }) + const inventoryServiceTx = this.inventoryService_.withTransaction( + manager + ) + for (const item of items) { - await this.inventoryService_ - .withTransaction(manager) - .adjustInventory(item.variant_id, -item.quantity) + await inventoryServiceTx.adjustInventory( + item.variant_id, + -item.quantity + ) } } @@ -759,12 +784,14 @@ class SwapService extends BaseService { const swapRepo = manager.getCustomRepository(this.swapRepository_) const result = await swapRepo.save(swap) + const shippingOptionServiceTx = this.shippingOptionService_.withTransaction( + manager + ) + for (const method of cart.shipping_methods) { - await this.shippingOptionService_ - .withTransaction(manager) - .updateShippingMethod(method.id, { - swap_id: result.id, - }) + await shippingOptionServiceTx.updateShippingMethod(method.id, { + swap_id: result.id, + }) } this.eventBus_ @@ -935,6 +962,8 @@ class SwapService extends BaseService { swap.fulfillment_status = "fulfilled" + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + // Update all line items to reflect fulfillment for (const item of swap.additional_items) { const fulfillmentItem = successfullyFulfilled.find( @@ -946,7 +975,7 @@ class SwapService extends BaseService { (item.fulfilled_quantity || 0) + fulfillmentItem.quantity // Update the fulfilled quantity - await this.lineItemService_.withTransaction(manager).update(item.id, { + await lineItemServiceTx.update(item.id, { fulfilled_quantity: fulfilledQuantity, }) @@ -1050,12 +1079,14 @@ class SwapService extends BaseService { swap.fulfillment_status = "shipped" + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + // Go through all the additional items in the swap for (const i of swap.additional_items) { const shipped = shipment.items.find((si) => 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, }) 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 f8254b99dc..0000000000 --- a/packages/medusa/src/services/system-payment-provider.js +++ /dev/null @@ -1,51 +0,0 @@ -import { BaseService } from "medusa-interfaces" - -class SystemProviderService extends BaseService { - 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 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/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..5ff720d5e4 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 @@ -89,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 @@ -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/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/__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/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/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index 1ffc99a066..0ff7ea662f 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 { EntityManager } from "typeorm" -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 OrderService from "../services/order" import SwapService from "../services/swap" +import { RequestContext } from "../types/request" -import { ICartCompletionStrategy, CartCompletionResponse } from "../interfaces" +import { + AbstractCartCompletionStrategy, + CartCompletionResponse, +} 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( @@ -42,164 +58,221 @@ class CartCompletionStrategy implements ICartCompletionStrategy { const swapService = this.swapService_ let inProgress = true - let err = false + let err: unknown = false 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 as IdempotencyKey + } + }) + 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 as IdempotencyKey + } + }) + 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: [ + "items", + "items.adjustments", + "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 +287,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 as IdempotencyKey + } + }) break } @@ -268,14 +308,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 } } 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/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({ 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/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 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 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 +} 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 +} 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 +} 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 +} 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 +} 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[] +} 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", 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") { 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 diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index 4dc879b7a6..0a1b7f5534 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" + }, ] }, ] 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: