From f1baca3cbd4805e42a4cd7d837fb5f74f1d3f7bc Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Tue, 26 Jan 2021 10:18:09 +0100 Subject: [PATCH] Replaces MongoDB support with PostgreSQL (#151) - All schemas have been rewritten to a relational model - All services have been rewritten to accommodate the new data model - Adds idempotency keys to core endpoints allowing you to retry requests with no additional side effects - Adds staged jobs to avoid putting jobs in the queue when transactions abort - Adds atomic transactions to all methods with access to the data layer Co-authored-by: Oliver Windall Juhl --- docs/api/store/endpoints/orders.yaml | 202 +- lerna-debug.log | 17 +- packages/babel-preset-medusa-package/index.js | 5 +- .../babel-preset-medusa-package/package.json | 7 +- packages/medusa-cli/package.json | 4 +- packages/medusa-cli/src/create-cli.js | 16 + packages/medusa-core-utils/package.json | 6 +- packages/medusa-core-utils/src/validator.js | 6 +- packages/medusa-core-utils/yarn.lock | 48 +- packages/medusa-file-spaces/package.json | 8 +- .../medusa-fulfillment-manual/package.json | 6 +- .../src/services/manual-fulfillment.js | 5 + .../package.json | 6 +- .../src/services/webshipper-fulfillment.js | 161 +- packages/medusa-interfaces/package.json | 11 +- .../src/__tests__/base-service.js | 33 + .../medusa-interfaces/src/base-service.js | 176 ++ .../src/fulfillment-service.js | 2 +- packages/medusa-payment-adyen/package.json | 8 +- .../api/routes/hooks/adyen-notification.js | 17 + .../src/api/routes/hooks/capture-hook.js | 41 - .../src/api/routes/hooks/index.js | 6 +- .../src/api/routes/store/authorize-payment.js | 58 - .../api/routes/store/check-payment-status.js | 30 - .../src/api/routes/store/index.js | 24 - .../routes/store/retrieve-payment-methods.js | 37 +- .../src/api/routes/store/update-payment.js | 32 - .../src/services/adyen.js | 407 ++- .../src/services/applepay-adyen.js | 104 + .../src/services/applepay.js | 74 - .../src/services/card-adyen.js | 62 + .../medusa-payment-adyen/src/services/card.js | 74 - .../src/services/googlepay-adyen.js | 58 + .../src/services/googlepay.js | 74 - .../src/services/ideal-adyen.js | 62 + .../src/services/ideal.js | 74 - .../src/services/mobilepay-adyen.js | 58 + .../src/services/mobilepay.js | 74 - .../src/services/paypal-adyen.js | 58 + .../src/services/paypal.js | 74 - .../src/subscribers/adyen.js | 159 ++ packages/medusa-payment-klarna/package.json | 8 +- .../src/__mocks__/cart.js | 31 +- .../src/api/routes/hooks/address.js | 119 +- .../src/api/routes/hooks/push.js | 20 +- .../src/api/routes/hooks/shipping.js | 56 +- .../src/services/__tests__/klarna-provider.js | 39 +- .../src/services/klarna-provider.js | 274 +- packages/medusa-payment-klarna/yarn.lock | 28 +- packages/medusa-payment-stripe/.babelrc | 1 + packages/medusa-payment-stripe/.gitignore | 3 +- packages/medusa-payment-stripe/.npmignore | 6 +- .../medusa-payment-stripe/__mocks__/cart.js | 107 +- packages/medusa-payment-stripe/package.json | 9 +- .../src/__mocks__/cart.js | 97 +- .../src/api/routes/hooks/stripe.js | 2 +- .../src/services/__mocks__/stripe-provider.js | 2 +- .../src/services/__tests__/stripe-provider.js | 31 +- .../src/services/stripe-provider.js | 270 +- .../src/subscribers/__tests__/cart.js | 93 - .../src/subscribers/cart.js | 79 +- packages/medusa-plugin-add-ons/package.json | 8 +- .../src/services/add-on.js | 13 +- .../medusa-plugin-brightpearl/package.json | 8 +- .../src/loaders/token-refresh.js | 25 - .../src/services/__tests__/brightpearl.js | 91 +- .../src/services/brightpearl.js | 260 +- .../src/subscribers/order.js | 92 +- packages/medusa-plugin-contentful/.npmignore | 6 +- .../medusa-plugin-contentful/package.json | 8 +- .../src/services/contentful.js | 156 +- .../src/subscribers/contentful.js | 9 +- .../package.json | 4 +- packages/medusa-plugin-economic/package.json | 8 +- .../src/services/economic.js | 2 +- packages/medusa-plugin-ip-lookup/.npmignore | 6 +- packages/medusa-plugin-ip-lookup/package.json | 4 +- .../src/api/medusa-middleware.js | 17 +- packages/medusa-plugin-mailchimp/package.json | 9 +- .../medusa-plugin-permissions/package.json | 8 +- packages/medusa-plugin-segment/package.json | 8 +- .../src/services/segment.js | 39 +- .../src/subscribers/order.js | 97 +- packages/medusa-plugin-sendgrid/.npmignore | 6 +- packages/medusa-plugin-sendgrid/package.json | 8 +- .../src/services/sendgrid.js | 3 +- .../src/subscribers/order.js | 174 +- .../.npmignore | 2 + .../package.json | 8 +- .../src/services/slack.js | 79 +- .../src/subscribers/order.js | 4 +- .../src/utils/eu-countries.js | 30 - .../utils/eu-countries.js | 8 - .../medusa-plugin-twilio-sms/package.json | 9 +- packages/medusa-plugin-wishlist/package.json | 8 +- packages/medusa-test-utils/package.json | 8 +- packages/medusa-test-utils/src/id-map.js | 22 +- packages/medusa-test-utils/src/index.js | 4 +- .../medusa-test-utils/src/mock-manager.js | 13 + .../medusa-test-utils/src/mock-repository.js | 67 + packages/medusa-test-utils/yarn.lock | 148 +- packages/medusa/.babelrc.js | 2 +- packages/medusa/ormconfig.json | 5 + packages/medusa/package.json | 23 +- .../src/api/middlewares/error-handler.js | 2 + .../api/routes/admin/auth/create-session.js | 2 +- .../routes/admin/customers/create-customer.js | 3 +- .../routes/admin/customers/get-customer.js | 24 +- .../routes/admin/customers/list-customers.js | 19 +- .../routes/admin/customers/update-customer.js | 13 +- .../admin/discounts/__tests__/add-region.js | 18 +- ...-valid-variant.js => add-valid-product.js} | 24 +- .../discounts/__tests__/create-discount.js | 13 +- .../admin/discounts/__tests__/get-discount.js | 27 +- .../discounts/__tests__/list-discounts.js | 32 - .../discounts/__tests__/remove-region.js | 27 +- ...lid-variant.js => remove-valid-product.js} | 35 +- .../discounts/__tests__/update-discount.js | 6 +- .../api/routes/admin/discounts/add-region.js | 10 +- .../admin/discounts/add-valid-product.js | 20 + .../admin/discounts/add-valid-variant.js | 15 - .../routes/admin/discounts/create-discount.js | 16 +- .../admin/discounts/create-dynamic-code.js | 7 +- .../routes/admin/discounts/delete-discount.js | 1 + .../admin/discounts/delete-dynamic-code.js | 13 +- .../routes/admin/discounts/get-discount.js | 24 +- .../src/api/routes/admin/discounts/index.js | 29 +- .../routes/admin/discounts/list-discounts.js | 36 +- .../routes/admin/discounts/remove-region.js | 9 +- .../admin/discounts/remove-valid-product.js | 20 + .../admin/discounts/remove-valid-variant.js | 14 - .../routes/admin/discounts/update-discount.js | 16 +- .../admin/gift-cards/create-gift-card.js | 34 + .../admin/gift-cards/delete-gift-card.js | 16 + .../routes/admin/gift-cards/get-gift-card.js | 17 + .../src/api/routes/admin/gift-cards/index.js | 52 + .../admin/gift-cards/list-gift-cards.js | 19 + .../admin/gift-cards/update-gift-card.js | 35 + packages/medusa/src/api/routes/admin/index.js | 8 + .../admin/orders/__tests__/archive-order.js | 2 +- .../admin/orders/__tests__/cancel-order.js | 2 +- .../admin/orders/__tests__/capture-payment.js | 2 +- .../orders/__tests__/create-fulfillment.js | 2 +- .../admin/orders/__tests__/get-order.js | 55 +- .../admin/orders/__tests__/return-order.js | 168 +- .../admin/orders/__tests__/set-metadata.js | 40 - .../admin/orders/add-shipping-method.js | 41 + .../api/routes/admin/orders/archive-order.js | 13 +- .../api/routes/admin/orders/cancel-order.js | 13 +- .../routes/admin/orders/capture-payment.js | 16 +- .../api/routes/admin/orders/complete-order.js | 13 +- .../routes/admin/orders/create-fulfillment.js | 18 +- .../routes/admin/orders/create-shipment.js | 15 +- .../admin/orders/create-swap-shipment.js | 15 +- .../api/routes/admin/orders/create-swap.js | 157 +- .../routes/admin/orders/delete-metadata.js | 8 +- .../api/routes/admin/orders/fulfill-swap.js | 24 +- .../src/api/routes/admin/orders/get-order.js | 13 +- .../src/api/routes/admin/orders/index.js | 104 +- .../api/routes/admin/orders/list-orders.js | 95 +- .../admin/orders/process-swap-payment.js | 20 +- .../api/routes/admin/orders/receive-return.js | 17 +- .../api/routes/admin/orders/receive-swap.js | 29 +- .../api/routes/admin/orders/refund-payment.js | 23 +- .../api/routes/admin/orders/request-return.js | 208 +- .../api/routes/admin/orders/update-order.js | 18 +- .../admin/products/__tests__/add-option.js | 5 +- .../products/__tests__/create-product.js | 19 +- .../products/__tests__/create-variant.js | 12 +- .../products/__tests__/delete-variant.js | 21 +- .../admin/products/__tests__/get-product.js | 36 +- .../admin/products/__tests__/get-variants.js | 39 - .../products/__tests__/publish-product.js | 38 - .../products/__tests__/update-variant.js | 131 +- .../api/routes/admin/products/add-option.js | 24 +- .../routes/admin/products/create-product.js | 175 +- .../routes/admin/products/create-variant.js | 71 +- .../routes/admin/products/delete-option.js | 22 +- .../routes/admin/products/delete-variant.js | 24 +- .../api/routes/admin/products/get-product.js | 22 +- .../src/api/routes/admin/products/index.js | 75 +- .../routes/admin/products/list-products.js | 70 +- .../routes/admin/products/publish-product.js | 27 - .../{orders => products}/set-metadata.js | 19 +- .../routes/admin/products/update-option.js | 25 +- .../routes/admin/products/update-product.js | 84 +- .../routes/admin/products/update-variant.js | 102 +- .../admin/regions/__tests__/get-region.js | 24 +- .../admin/regions/__tests__/list-regions.js | 28 +- .../api/routes/admin/regions/add-country.js | 9 +- .../admin/regions/add-fulfillment-provider.js | 6 +- .../admin/regions/add-payment-provider.js | 6 +- .../api/routes/admin/regions/create-region.js | 11 +- .../routes/admin/regions/delete-metadata.js | 9 +- .../admin/regions/get-fulfillment-options.js | 11 +- .../api/routes/admin/regions/get-region.js | 6 +- .../src/api/routes/admin/regions/index.js | 18 + .../api/routes/admin/regions/list-regions.js | 19 +- .../routes/admin/regions/remove-country.js | 10 +- .../regions/remove-fulfillment-provider.js | 14 +- .../admin/regions/remove-payment-provider.js | 13 +- .../api/routes/admin/regions/set-metadata.js | 7 +- .../api/routes/admin/regions/update-region.js | 11 +- .../src/api/routes/admin/returns/index.js | 15 + .../api/routes/admin/returns/list-returns.js | 25 + .../__tests__/create-shipping-option.js | 24 +- .../__tests__/list-shipping-options.js | 26 +- .../__tests__/update-shipping-option.js | 16 +- .../create-shipping-option.js | 27 +- .../delete-shipping-option.js | 2 - .../routes/admin/shipping-options/index.js | 18 + .../shipping-options/list-shipping-options.js | 8 +- .../update-shipping-option.js | 25 +- .../__tests__/add-product.js | 39 - .../__tests__/add-shipping-option.js | 41 - .../__tests__/get-shipping-profile.js | 18 +- .../__tests__/remove-product.js | 37 - .../__tests__/remove-shipping-option.js | 38 - .../__tests__/update-shipping-profile.js | 4 - .../admin/shipping-profiles/add-product.js | 21 - .../shipping-profiles/add-shipping-option.js | 24 - .../shipping-profiles/get-shipping-profile.js | 12 +- .../routes/admin/shipping-profiles/index.js | 32 +- .../admin/shipping-profiles/remove-product.js | 14 - .../remove-shipping-option.js | 14 - .../update-shipping-profile.js | 2 - .../routes/admin/store/__tests__/get-store.js | 5 +- .../src/api/routes/admin/store/get-store.js | 14 +- .../src/api/routes/admin/store/index.js | 4 + .../admin/store/list-payment-providers.js | 9 + .../api/routes/admin/store/update-store.js | 6 +- .../src/api/routes/admin/swaps/get-swap.js | 24 + .../src/api/routes/admin/swaps/index.js | 20 + .../src/api/routes/admin/swaps/list-swaps.js | 24 + .../admin/users/__tests__/set-password.js | 46 - .../src/api/routes/admin/users/index.js | 4 - .../api/routes/admin/users/set-password.js | 21 - .../src/api/routes/admin/variants/index.js | 62 + .../routes/admin/variants/list-variants.js | 30 + .../api/routes/store/auth/create-session.js | 11 +- .../src/api/routes/store/auth/get-session.js | 21 +- .../carts/__tests__/add-shipping-method.js | 14 +- .../store/carts/__tests__/complete-cart.js | 120 + .../store/carts/__tests__/create-cart.js | 36 +- .../store/carts/__tests__/create-line-item.js | 7 +- .../__tests__/create-payment-sessions.js | 7 +- .../routes/store/carts/__tests__/get-cart.js | 12 +- .../__tests__/refresh-payment-session.js | 42 + .../store/carts/__tests__/update-cart.js | 89 +- .../store/carts/__tests__/update-line-item.js | 67 +- .../carts/__tests__/update-payment-method.js | 3 +- .../carts/__tests__/update-payment-session.js | 50 + .../routes/store/carts/add-shipping-method.js | 21 +- .../api/routes/store/carts/complete-cart.js | 203 ++ .../src/api/routes/store/carts/create-cart.js | 92 +- .../routes/store/carts/create-line-item.js | 33 +- .../store/carts/create-payment-sessions.js | 11 +- .../api/routes/store/carts/delete-discount.js | 24 +- .../routes/store/carts/delete-line-item.js | 24 +- .../store/carts/delete-payment-session.js | 9 +- .../src/api/routes/store/carts/get-cart.js | 20 +- .../src/api/routes/store/carts/index.js | 45 + .../store/carts/refresh-payment-session.js | 30 + .../routes/store/carts/set-payment-session.js | 29 + .../src/api/routes/store/carts/update-cart.js | 74 +- .../routes/store/carts/update-line-item.js | 61 +- .../store/carts/update-payment-method.js | 6 +- .../store/carts/update-payment-session.js | 30 + .../customers/__tests__/create-customer.js | 12 +- .../customers/__tests__/reset-password.js | 14 +- .../customers/__tests__/update-customer.js | 15 +- .../routes/store/customers/create-address.js | 20 +- .../routes/store/customers/create-customer.js | 12 +- .../routes/store/customers/delete-address.js | 12 +- .../routes/store/customers/get-customer.js | 9 +- .../store/customers/get-payment-methods.js | 2 +- .../store/customers/reset-password-token.js | 2 +- .../routes/store/customers/reset-password.js | 11 +- .../routes/store/customers/update-address.js | 16 +- .../routes/store/customers/update-customer.js | 15 +- .../store/orders/__tests__/create-order.js | 27 - .../orders/__tests__/get-order-by-cart.js | 35 + .../store/orders/__tests__/get-order.js | 6 +- .../api/routes/store/orders/create-order.js | 59 - .../routes/store/orders/get-order-by-cart.js | 17 + .../src/api/routes/store/orders/get-order.js | 32 +- .../src/api/routes/store/orders/index.js | 36 +- .../store/products/__tests__/get-product.js | 8 +- .../store/products/__tests__/list-products.js | 29 +- .../api/routes/store/products/get-product.js | 30 +- .../src/api/routes/store/products/index.js | 2 +- .../routes/store/products/list-products.js | 46 +- .../store/regions/__tests__/get-region.js | 37 + .../store/regions/__tests__/list-regions.js | 36 + .../api/routes/store/regions/get-region.js | 25 +- .../api/routes/store/regions/list-regions.js | 29 +- .../__tests__/list-options.js | 40 + .../__tests__/list-shipping-options.js | 15 +- .../store/shipping-options/list-options.js | 25 +- .../shipping-options/list-shipping-options.js | 6 +- .../src/api/routes/store/swaps/create-swap.js | 2 +- .../store/variants/__tests__/get-variant.js | 26 + .../store/variants/__tests__/list-variants.js | 21 + .../api/routes/store/variants/get-variant.js | 32 +- .../src/api/routes/store/variants/index.js | 2 +- .../routes/store/variants/list-variants.js | 36 +- packages/medusa/src/app.js | 1 + packages/medusa/src/commands/migrate.js | 155 ++ packages/medusa/src/helpers/test-request.js | 2 + packages/medusa/src/loaders/database.js | 18 + packages/medusa/src/loaders/defaults.js | 31 +- packages/medusa/src/loaders/index.js | 33 +- packages/medusa/src/loaders/models.js | 21 +- packages/medusa/src/loaders/mongoose.js | 20 - packages/medusa/src/loaders/repositories.js | 45 + .../1611063162649-initial_schema.ts | 382 +++ .../1611063174563-countries_currencies.ts | 44 + packages/medusa/src/models/address.ts | 81 + packages/medusa/src/models/cart.js | 38 - packages/medusa/src/models/cart.ts | 179 ++ packages/medusa/src/models/counter.js | 13 - packages/medusa/src/models/country.ts | 42 + packages/medusa/src/models/currency.ts | 16 + packages/medusa/src/models/customer.js | 27 - packages/medusa/src/models/customer.ts | 80 + packages/medusa/src/models/discount-rule.ts | 87 + packages/medusa/src/models/discount.js | 24 - packages/medusa/src/models/discount.ts | 89 + packages/medusa/src/models/document.js | 16 - .../src/models/dynamic-discount-code.js | 16 - .../medusa/src/models/fulfillment-item.ts | 40 + .../medusa/src/models/fulfillment-provider.ts | 10 + packages/medusa/src/models/fulfillment.ts | 93 + .../src/models/gift-card-transaction.ts | 48 + packages/medusa/src/models/gift-card.ts | 75 + packages/medusa/src/models/idempotency-key.ts | 49 + packages/medusa/src/models/image.ts | 38 + packages/medusa/src/models/line-item.ts | 123 + packages/medusa/src/models/money-amount.ts | 70 + packages/medusa/src/models/oauth.js | 16 - packages/medusa/src/models/oauth.ts | 47 + packages/medusa/src/models/order.js | 46 - packages/medusa/src/models/order.ts | 244 ++ .../medusa/src/models/payment-provider.ts | 10 + packages/medusa/src/models/payment-session.ts | 66 + packages/medusa/src/models/payment.ts | 91 + .../medusa/src/models/product-option-value.ts | 65 + packages/medusa/src/models/product-option.ts | 57 + packages/medusa/src/models/product-variant.js | 30 - packages/medusa/src/models/product-variant.ts | 122 + packages/medusa/src/models/product.js | 27 - packages/medusa/src/models/product.ts | 127 + packages/medusa/src/models/refund.ts | 66 + packages/medusa/src/models/region.js | 18 - packages/medusa/src/models/region.ts | 95 + packages/medusa/src/models/return-item.ts | 52 + packages/medusa/src/models/return.ts | 99 + packages/medusa/src/models/schemas/address.js | 17 - .../src/models/schemas/discount-rule.js | 28 - .../medusa/src/models/schemas/discount.js | 15 - .../medusa/src/models/schemas/fulfillment.js | 13 - .../medusa/src/models/schemas/line-item.js | 53 - .../medusa/src/models/schemas/option-value.js | 6 - packages/medusa/src/models/schemas/option.js | 10 - .../src/models/schemas/payment-method.js | 9 - packages/medusa/src/models/schemas/refund.js | 9 - .../src/models/schemas/return-line-item.js | 23 - packages/medusa/src/models/schemas/return.js | 15 - .../medusa/src/models/schemas/shipment.js | 7 - .../src/models/schemas/shipping-method.js | 10 - .../models/schemas/shipping-option-price.js | 6 - .../schemas/shipping-option-requirement.js | 6 - packages/medusa/src/models/shipping-method.ts | 84 + .../src/models/shipping-option-requirement.ts | 50 + packages/medusa/src/models/shipping-option.js | 22 - packages/medusa/src/models/shipping-option.ts | 97 + .../medusa/src/models/shipping-profile.js | 13 - .../medusa/src/models/shipping-profile.ts | 69 + packages/medusa/src/models/staged-job.ts | 35 + packages/medusa/src/models/store.js | 17 - packages/medusa/src/models/store.ts | 67 + packages/medusa/src/models/swap.js | 36 - packages/medusa/src/models/swap.ts | 143 ++ packages/medusa/src/models/user.js | 19 - packages/medusa/src/models/user.ts | 52 + .../__mocks__/cart.js | 0 .../__mocks__/customer.js | 0 .../__mocks__/discount.js | 22 +- .../__mocks__/document.js | 0 .../__mocks__/dynamic-discount-code.js | 0 .../__mocks__/order.js | 0 .../__mocks__/product-variant.js | 0 .../__mocks__/product.js | 0 .../__mocks__/region.js | 0 .../__mocks__/shipping-option.js | 0 .../__mocks__/shipping-profile.js | 0 .../__mocks__/store.js | 0 .../__mocks__/user.js | 0 packages/medusa/src/repositories/address.ts | 5 + packages/medusa/src/repositories/cart.ts | 5 + packages/medusa/src/repositories/country.ts | 5 + packages/medusa/src/repositories/currency.ts | 5 + packages/medusa/src/repositories/customer.ts | 5 + .../medusa/src/repositories/discount-rule.ts | 5 + packages/medusa/src/repositories/discount.ts | 5 + .../src/repositories/fulfillment-provider.ts | 7 + .../medusa/src/repositories/fulfillment.ts | 5 + .../src/repositories/gift-card-transaction.ts | 7 + packages/medusa/src/repositories/gift-card.ts | 5 + .../src/repositories/idempotency-key.ts | 5 + packages/medusa/src/repositories/line-item.ts | 5 + .../medusa/src/repositories/money-amount.ts | 5 + packages/medusa/src/repositories/oauth.ts | 5 + packages/medusa/src/repositories/order.ts | 5 + .../src/repositories/payment-provider.ts | 5 + .../src/repositories/payment-session.ts | 5 + packages/medusa/src/repositories/payment.ts | 5 + .../src/repositories/product-option-value.ts | 5 + .../medusa/src/repositories/product-option.ts | 5 + .../src/repositories/product-variant.ts | 5 + packages/medusa/src/repositories/product.ts | 5 + packages/medusa/src/repositories/refund.ts | 5 + packages/medusa/src/repositories/region.ts | 5 + .../medusa/src/repositories/return-item.ts | 5 + packages/medusa/src/repositories/return.ts | 5 + .../src/repositories/shipping-method.ts | 5 + .../shipping-option-requirement.ts | 5 + .../src/repositories/shipping-option.ts | 5 + .../src/repositories/shipping-profile.ts | 5 + .../medusa/src/repositories/staged-job.ts | 5 + packages/medusa/src/repositories/store.ts | 5 + packages/medusa/src/repositories/swap.ts | 5 + packages/medusa/src/repositories/user.ts | 5 + .../medusa/src/scripts/mongo-sql-migration.js | 1098 ++++++++ .../medusa/src/services/__mocks__/cart.js | 164 +- .../medusa/src/services/__mocks__/counter.js | 11 - .../medusa/src/services/__mocks__/customer.js | 10 +- .../medusa/src/services/__mocks__/discount.js | 136 +- .../__mocks__/fulfillment-provider.js | 3 + .../src/services/__mocks__/idempotency-key.js | 42 + .../src/services/__mocks__/line-item.js | 25 +- .../medusa/src/services/__mocks__/order.js | 49 +- .../services/__mocks__/payment-provider.js | 6 + .../src/services/__mocks__/product-variant.js | 46 +- .../medusa/src/services/__mocks__/product.js | 30 +- .../medusa/src/services/__mocks__/region.js | 16 +- .../medusa/src/services/__mocks__/return.js | 14 + .../services/__mocks__/shipping-profile.js | 23 +- .../medusa/src/services/__mocks__/swap.js | 22 + .../medusa/src/services/__tests__/cart.js | 2255 +++++++++-------- .../medusa/src/services/__tests__/customer.js | 398 +-- .../medusa/src/services/__tests__/discount.js | 572 +++-- .../medusa/src/services/__tests__/document.js | 24 - .../src/services/__tests__/event-bus.js | 87 +- .../src/services/__tests__/fulfillment.js | 129 + .../src/services/__tests__/line-item.js | 306 ++- .../medusa/src/services/__tests__/order.js | 2121 ++++++++-------- .../services/__tests__/payment-provider.js | 17 + .../src/services/__tests__/product-variant.js | 1252 +++++---- .../medusa/src/services/__tests__/product.js | 1136 +++------ .../medusa/src/services/__tests__/region.js | 850 ++++--- .../medusa/src/services/__tests__/return.js | 274 ++ .../src/services/__tests__/shipping-option.js | 602 +++-- .../services/__tests__/shipping-profile.js | 535 ++-- .../medusa/src/services/__tests__/store.js | 151 +- .../medusa/src/services/__tests__/swap.js | 713 +++--- .../medusa/src/services/__tests__/totals.js | 549 ++-- .../medusa/src/services/__tests__/user.js | 184 +- packages/medusa/src/services/auth.js | 1 + packages/medusa/src/services/cart.js | 1903 +++++++------- packages/medusa/src/services/counter.js | 33 - packages/medusa/src/services/customer.js | 403 +-- packages/medusa/src/services/discount.js | 516 ++-- packages/medusa/src/services/document.js | 138 - packages/medusa/src/services/event-bus.js | 166 +- .../src/services/fulfillment-provider.js | 53 + packages/medusa/src/services/fulfillment.js | 239 +- packages/medusa/src/services/gift-card.js | 259 ++ .../medusa/src/services/idempotency-key.js | 178 ++ packages/medusa/src/services/line-item.js | 293 ++- packages/medusa/src/services/oauth.js | 39 +- packages/medusa/src/services/order.js | 1754 +++++++------ .../medusa/src/services/payment-provider.js | 384 ++- .../medusa/src/services/product-variant.js | 811 +++--- packages/medusa/src/services/product.js | 946 +++---- packages/medusa/src/services/region.js | 528 ++-- packages/medusa/src/services/return.js | 449 +++- .../medusa/src/services/shipping-option.js | 579 +++-- .../medusa/src/services/shipping-profile.js | 517 ++-- packages/medusa/src/services/store.js | 262 +- packages/medusa/src/services/swap.js | 976 +++---- packages/medusa/src/services/totals.js | 183 +- packages/medusa/src/services/transaction.js | 15 + packages/medusa/src/services/user.js | 266 +- packages/medusa/src/subscribers/order.js | 93 +- packages/medusa/src/utils/countries.js | 1 - packages/medusa/src/utils/naming-strategy.ts | 13 + packages/medusa/tsconfig.json | 15 + packages/medusa/yarn.lock | 819 +++++- 499 files changed, 25909 insertions(+), 16128 deletions(-) create mode 100644 packages/medusa-payment-adyen/src/api/routes/hooks/adyen-notification.js delete mode 100644 packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js delete mode 100644 packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js delete mode 100644 packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js delete mode 100644 packages/medusa-payment-adyen/src/api/routes/store/update-payment.js create mode 100644 packages/medusa-payment-adyen/src/services/applepay-adyen.js delete mode 100644 packages/medusa-payment-adyen/src/services/applepay.js create mode 100644 packages/medusa-payment-adyen/src/services/card-adyen.js delete mode 100644 packages/medusa-payment-adyen/src/services/card.js create mode 100644 packages/medusa-payment-adyen/src/services/googlepay-adyen.js delete mode 100644 packages/medusa-payment-adyen/src/services/googlepay.js create mode 100644 packages/medusa-payment-adyen/src/services/ideal-adyen.js delete mode 100644 packages/medusa-payment-adyen/src/services/ideal.js create mode 100644 packages/medusa-payment-adyen/src/services/mobilepay-adyen.js delete mode 100644 packages/medusa-payment-adyen/src/services/mobilepay.js create mode 100644 packages/medusa-payment-adyen/src/services/paypal-adyen.js delete mode 100644 packages/medusa-payment-adyen/src/services/paypal.js create mode 100644 packages/medusa-payment-adyen/src/subscribers/adyen.js delete mode 100644 packages/medusa-payment-stripe/src/subscribers/__tests__/cart.js delete mode 100644 packages/medusa-plugin-brightpearl/src/loaders/token-refresh.js delete mode 100644 packages/medusa-plugin-slack-notification/src/utils/eu-countries.js delete mode 100644 packages/medusa-plugin-slack-notification/utils/eu-countries.js create mode 100644 packages/medusa-test-utils/src/mock-manager.js create mode 100644 packages/medusa-test-utils/src/mock-repository.js create mode 100644 packages/medusa/ormconfig.json rename packages/medusa/src/api/routes/admin/discounts/__tests__/{add-valid-variant.js => add-valid-product.js} (60%) rename packages/medusa/src/api/routes/admin/discounts/__tests__/{remove-valid-variant.js => remove-valid-product.js} (58%) create mode 100644 packages/medusa/src/api/routes/admin/discounts/add-valid-product.js delete mode 100644 packages/medusa/src/api/routes/admin/discounts/add-valid-variant.js create mode 100644 packages/medusa/src/api/routes/admin/discounts/remove-valid-product.js delete mode 100644 packages/medusa/src/api/routes/admin/discounts/remove-valid-variant.js create mode 100644 packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.js create mode 100644 packages/medusa/src/api/routes/admin/gift-cards/delete-gift-card.js create mode 100644 packages/medusa/src/api/routes/admin/gift-cards/get-gift-card.js create mode 100644 packages/medusa/src/api/routes/admin/gift-cards/index.js create mode 100644 packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js create mode 100644 packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js delete mode 100644 packages/medusa/src/api/routes/admin/orders/__tests__/set-metadata.js create mode 100644 packages/medusa/src/api/routes/admin/orders/add-shipping-method.js delete mode 100644 packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js delete mode 100644 packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js delete mode 100644 packages/medusa/src/api/routes/admin/products/publish-product.js rename packages/medusa/src/api/routes/admin/{orders => products}/set-metadata.js (54%) create mode 100644 packages/medusa/src/api/routes/admin/returns/index.js create mode 100644 packages/medusa/src/api/routes/admin/returns/list-returns.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-product.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-shipping-option.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-product.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-shipping-option.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/add-product.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/add-shipping-option.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/remove-product.js delete mode 100644 packages/medusa/src/api/routes/admin/shipping-profiles/remove-shipping-option.js create mode 100644 packages/medusa/src/api/routes/admin/store/list-payment-providers.js create mode 100644 packages/medusa/src/api/routes/admin/swaps/get-swap.js create mode 100644 packages/medusa/src/api/routes/admin/swaps/index.js create mode 100644 packages/medusa/src/api/routes/admin/swaps/list-swaps.js delete mode 100644 packages/medusa/src/api/routes/admin/users/__tests__/set-password.js delete mode 100644 packages/medusa/src/api/routes/admin/users/set-password.js create mode 100644 packages/medusa/src/api/routes/admin/variants/index.js create mode 100644 packages/medusa/src/api/routes/admin/variants/list-variants.js create mode 100644 packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js create mode 100644 packages/medusa/src/api/routes/store/carts/__tests__/refresh-payment-session.js create mode 100644 packages/medusa/src/api/routes/store/carts/__tests__/update-payment-session.js create mode 100644 packages/medusa/src/api/routes/store/carts/complete-cart.js create mode 100644 packages/medusa/src/api/routes/store/carts/refresh-payment-session.js create mode 100644 packages/medusa/src/api/routes/store/carts/set-payment-session.js create mode 100644 packages/medusa/src/api/routes/store/carts/update-payment-session.js delete mode 100644 packages/medusa/src/api/routes/store/orders/__tests__/create-order.js create mode 100644 packages/medusa/src/api/routes/store/orders/__tests__/get-order-by-cart.js delete mode 100644 packages/medusa/src/api/routes/store/orders/create-order.js create mode 100644 packages/medusa/src/api/routes/store/orders/get-order-by-cart.js create mode 100644 packages/medusa/src/api/routes/store/regions/__tests__/get-region.js create mode 100644 packages/medusa/src/api/routes/store/regions/__tests__/list-regions.js create mode 100644 packages/medusa/src/api/routes/store/shipping-options/__tests__/list-options.js create mode 100644 packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js create mode 100644 packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js create mode 100644 packages/medusa/src/commands/migrate.js create mode 100644 packages/medusa/src/loaders/database.js delete mode 100644 packages/medusa/src/loaders/mongoose.js create mode 100644 packages/medusa/src/loaders/repositories.js create mode 100644 packages/medusa/src/migrations/1611063162649-initial_schema.ts create mode 100644 packages/medusa/src/migrations/1611063174563-countries_currencies.ts create mode 100644 packages/medusa/src/models/address.ts delete mode 100644 packages/medusa/src/models/cart.js create mode 100644 packages/medusa/src/models/cart.ts delete mode 100644 packages/medusa/src/models/counter.js create mode 100644 packages/medusa/src/models/country.ts create mode 100644 packages/medusa/src/models/currency.ts delete mode 100644 packages/medusa/src/models/customer.js create mode 100644 packages/medusa/src/models/customer.ts create mode 100644 packages/medusa/src/models/discount-rule.ts delete mode 100644 packages/medusa/src/models/discount.js create mode 100644 packages/medusa/src/models/discount.ts delete mode 100644 packages/medusa/src/models/document.js delete mode 100644 packages/medusa/src/models/dynamic-discount-code.js create mode 100644 packages/medusa/src/models/fulfillment-item.ts create mode 100644 packages/medusa/src/models/fulfillment-provider.ts create mode 100644 packages/medusa/src/models/fulfillment.ts create mode 100644 packages/medusa/src/models/gift-card-transaction.ts create mode 100644 packages/medusa/src/models/gift-card.ts create mode 100644 packages/medusa/src/models/idempotency-key.ts create mode 100644 packages/medusa/src/models/image.ts create mode 100644 packages/medusa/src/models/line-item.ts create mode 100644 packages/medusa/src/models/money-amount.ts delete mode 100644 packages/medusa/src/models/oauth.js create mode 100644 packages/medusa/src/models/oauth.ts delete mode 100644 packages/medusa/src/models/order.js create mode 100644 packages/medusa/src/models/order.ts create mode 100644 packages/medusa/src/models/payment-provider.ts create mode 100644 packages/medusa/src/models/payment-session.ts create mode 100644 packages/medusa/src/models/payment.ts create mode 100644 packages/medusa/src/models/product-option-value.ts create mode 100644 packages/medusa/src/models/product-option.ts delete mode 100644 packages/medusa/src/models/product-variant.js create mode 100644 packages/medusa/src/models/product-variant.ts delete mode 100644 packages/medusa/src/models/product.js create mode 100644 packages/medusa/src/models/product.ts create mode 100644 packages/medusa/src/models/refund.ts delete mode 100644 packages/medusa/src/models/region.js create mode 100644 packages/medusa/src/models/region.ts create mode 100644 packages/medusa/src/models/return-item.ts create mode 100644 packages/medusa/src/models/return.ts delete mode 100644 packages/medusa/src/models/schemas/address.js delete mode 100644 packages/medusa/src/models/schemas/discount-rule.js delete mode 100644 packages/medusa/src/models/schemas/discount.js delete mode 100644 packages/medusa/src/models/schemas/fulfillment.js delete mode 100644 packages/medusa/src/models/schemas/line-item.js delete mode 100644 packages/medusa/src/models/schemas/option-value.js delete mode 100644 packages/medusa/src/models/schemas/option.js delete mode 100644 packages/medusa/src/models/schemas/payment-method.js delete mode 100644 packages/medusa/src/models/schemas/refund.js delete mode 100644 packages/medusa/src/models/schemas/return-line-item.js delete mode 100644 packages/medusa/src/models/schemas/return.js delete mode 100644 packages/medusa/src/models/schemas/shipment.js delete mode 100644 packages/medusa/src/models/schemas/shipping-method.js delete mode 100644 packages/medusa/src/models/schemas/shipping-option-price.js delete mode 100644 packages/medusa/src/models/schemas/shipping-option-requirement.js create mode 100644 packages/medusa/src/models/shipping-method.ts create mode 100644 packages/medusa/src/models/shipping-option-requirement.ts delete mode 100644 packages/medusa/src/models/shipping-option.js create mode 100644 packages/medusa/src/models/shipping-option.ts delete mode 100644 packages/medusa/src/models/shipping-profile.js create mode 100644 packages/medusa/src/models/shipping-profile.ts create mode 100644 packages/medusa/src/models/staged-job.ts delete mode 100644 packages/medusa/src/models/store.js create mode 100644 packages/medusa/src/models/store.ts delete mode 100644 packages/medusa/src/models/swap.js create mode 100644 packages/medusa/src/models/swap.ts delete mode 100644 packages/medusa/src/models/user.js create mode 100644 packages/medusa/src/models/user.ts rename packages/medusa/src/{models => repositories}/__mocks__/cart.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/customer.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/discount.js (94%) rename packages/medusa/src/{models => repositories}/__mocks__/document.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/dynamic-discount-code.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/order.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/product-variant.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/product.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/region.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/shipping-option.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/shipping-profile.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/store.js (100%) rename packages/medusa/src/{models => repositories}/__mocks__/user.js (100%) create mode 100644 packages/medusa/src/repositories/address.ts create mode 100644 packages/medusa/src/repositories/cart.ts create mode 100644 packages/medusa/src/repositories/country.ts create mode 100644 packages/medusa/src/repositories/currency.ts create mode 100644 packages/medusa/src/repositories/customer.ts create mode 100644 packages/medusa/src/repositories/discount-rule.ts create mode 100644 packages/medusa/src/repositories/discount.ts create mode 100644 packages/medusa/src/repositories/fulfillment-provider.ts create mode 100644 packages/medusa/src/repositories/fulfillment.ts create mode 100644 packages/medusa/src/repositories/gift-card-transaction.ts create mode 100644 packages/medusa/src/repositories/gift-card.ts create mode 100644 packages/medusa/src/repositories/idempotency-key.ts create mode 100644 packages/medusa/src/repositories/line-item.ts create mode 100644 packages/medusa/src/repositories/money-amount.ts create mode 100644 packages/medusa/src/repositories/oauth.ts create mode 100644 packages/medusa/src/repositories/order.ts create mode 100644 packages/medusa/src/repositories/payment-provider.ts create mode 100644 packages/medusa/src/repositories/payment-session.ts create mode 100644 packages/medusa/src/repositories/payment.ts create mode 100644 packages/medusa/src/repositories/product-option-value.ts create mode 100644 packages/medusa/src/repositories/product-option.ts create mode 100644 packages/medusa/src/repositories/product-variant.ts create mode 100644 packages/medusa/src/repositories/product.ts create mode 100644 packages/medusa/src/repositories/refund.ts create mode 100644 packages/medusa/src/repositories/region.ts create mode 100644 packages/medusa/src/repositories/return-item.ts create mode 100644 packages/medusa/src/repositories/return.ts create mode 100644 packages/medusa/src/repositories/shipping-method.ts create mode 100644 packages/medusa/src/repositories/shipping-option-requirement.ts create mode 100644 packages/medusa/src/repositories/shipping-option.ts create mode 100644 packages/medusa/src/repositories/shipping-profile.ts create mode 100644 packages/medusa/src/repositories/staged-job.ts create mode 100644 packages/medusa/src/repositories/store.ts create mode 100644 packages/medusa/src/repositories/swap.ts create mode 100644 packages/medusa/src/repositories/user.ts create mode 100644 packages/medusa/src/scripts/mongo-sql-migration.js delete mode 100644 packages/medusa/src/services/__mocks__/counter.js create mode 100644 packages/medusa/src/services/__mocks__/idempotency-key.js create mode 100644 packages/medusa/src/services/__mocks__/return.js create mode 100644 packages/medusa/src/services/__mocks__/swap.js delete mode 100644 packages/medusa/src/services/__tests__/document.js create mode 100644 packages/medusa/src/services/__tests__/fulfillment.js create mode 100644 packages/medusa/src/services/__tests__/return.js delete mode 100644 packages/medusa/src/services/counter.js delete mode 100644 packages/medusa/src/services/document.js create mode 100644 packages/medusa/src/services/gift-card.js create mode 100644 packages/medusa/src/services/idempotency-key.js create mode 100644 packages/medusa/src/services/transaction.js create mode 100644 packages/medusa/src/utils/naming-strategy.ts create mode 100644 packages/medusa/tsconfig.json diff --git a/docs/api/store/endpoints/orders.yaml b/docs/api/store/endpoints/orders.yaml index a6257b1b9f..29bb53634e 100644 --- a/docs/api/store/endpoints/orders.yaml +++ b/docs/api/store/endpoints/orders.yaml @@ -7,12 +7,12 @@ routes: path: /orders/:id route: /orders description: > - An order represents a selection of items purchased and holds information about + An Order represents a selection of items purchased and holds information about how the items have been purchased and will be fulfilled. endpoints: - path: /:id method: GET - title: Retrieve an order + title: Retrieve an Order params: - name: id type: String @@ -34,104 +34,104 @@ response: | { "order": { "id": "order_fi13oadpo2r3vc2g4592", - "status": "pending", - "fulfillment_status": "shipped", - "payment_status": "captured", - "email": "iron@man.com", - "billing_address": { - "id": "addr_WgNn0BSfIu", - "customer_id": "cus_4eThzYSuGv", - "company": "Stark Industries", - "first_name": "Tony", - "last_name": "Stark", - "address_1": "Hollywood Boulevard 1", - "address_2": null, - "city": "Los Angeles", - "country_code": "US", - "province": "CA", - "postal_code": "90046", - "phone": null, - "created_at": "2020-12-11T17:03:54.458Z", - "updated_at": "2020-12-11T17:03:54.458Z", - "deleted_at": null, - "metadata": null - }, - "shipping_address": { - "id": "addr_WgNn0BSfIu", - "customer_id": "cus_4eThzYSuGv", - "company": "Stark Industries", - "first_name": "Tony", - "last_name": "Stark", - "address_1": "Hollywood Boulevard 1", - "address_2": null, - "city": "Los Angeles", - "country_code": "US", - "province": "CA", - "postal_code": "90046", - "phone": null, - "created_at": "2020-12-11T17:03:54.458Z", - "updated_at": "2020-12-11T17:03:54.458Z", - "deleted_at": null, - "metadata": null - }, - "items": [ - { - "id": "item_fn2uaQH95vG9ZMnhj2aU03xg", - "cart_id": null, - "order_id": "order_s9RojwCU2AM8RztcldM2Uof7", - "swap_id": null, - "title": "Ironman suit", - "description": "Awesome Ironman suit", - "thumbnail": null, - "is_giftcard": false, - "should_merge": false, - "allow_discounts": true, - "unit_price": 119600, - "variant_id": "variant_rdEH6PykBuH57giw", - "quantity": 1, - "fulfilled_quantity": 1, - "returned_quantity": 0, - "shipped_quantity": 0, - "created_at": "2020-12-11T17:03:54.458Z", - "updated_at": "2020-12-11T17:03:54.458Z", - "metadata": null - } - ], - "discounts": [ - { - "id": "disc_OpYQN4H8MOWHN2en", - "code": "CCC3C4LL88", - "is_dynamic": true, - "discount_rule_id": "dru_MDyr3lJLa00uxOsY", - "parent_discount_id": "disc_ubtdlkASI7bfUj81", - "metadata": null - } - ], - "customer_id": "cus_4eThzYSuGv", - "shipping_methods": [ - { - "id": "sm_77bEapbO8tkCqw3yo1NBuCUl", - "shipping_option_id": "so_nRvwHhEdZw", - "order_id": "order_s9RojwCU2AM8RztcldM2Uof7", - "cart_id": null, - "swap_id": null, - "return_id": null, - "price": 0, - "data": { - "id": "Parcel shop", - "city": "Los Angeles", - "postal": "90046" - } - } - ], - "metadata": null, - "display_id": 50433, - "currency_code": "dkk", - "region_id": "reg_HMnixPlOicAs7aBlXuchAGxd", - "shipping_total": 0, - "discount_total": 0, - "tax_total": 3850000, - "subtotal": 154000, - "total": 4004000 + "status": "pending", + "fulfillment_status": "shipped", + "payment_status": "captured", + "email": "iron@man.com", + "billing_address": { + "id": "addr_WgNn0BSfIu", + "customer_id": "cus_4eThzYSuGv", + "company": "Stark Industries", + "first_name": "Tony", + "last_name": "Stark", + "address_1": "Hollywood Boulevard 1", + "address_2": null, + "city": "Los Angeles", + "country_code": "US", + "province": "CA", + "postal_code": "90046", + "phone": null, + "created_at": "2020-12-11T17:03:54.458Z", + "updated_at": "2020-12-11T17:03:54.458Z", + "deleted_at": null, + "metadata": null + }, + "shipping_address": { + "id": "addr_WgNn0BSfIu", + "customer_id": "cus_4eThzYSuGv", + "company": "Stark Industries", + "first_name": "Tony", + "last_name": "Stark", + "address_1": "Hollywood Boulevard 1", + "address_2": null, + "city": "Los Angeles", + "country_code": "US", + "province": "CA", + "postal_code": "90046", + "phone": null, + "created_at": "2020-12-11T17:03:54.458Z", + "updated_at": "2020-12-11T17:03:54.458Z", + "deleted_at": null, + "metadata": null + }, + "items": [ + { + "id": "item_fn2uaQH95vG9ZMnhj2aU03xg", + "cart_id": null, + "order_id": "order_s9RojwCU2AM8RztcldM2Uof7", + "swap_id": null, + "title": "Ironman suit", + "description": "Awesome Ironman suit", + "thumbnail": null, + "is_giftcard": false, + "should_merge": false, + "allow_discounts": true, + "unit_price": 119600, + "variant_id": "variant_rdEH6PykBuH57giw", + "quantity": 1, + "fulfilled_quantity": 1, + "returned_quantity": 0, + "shipped_quantity": 0, + "created_at": "2020-12-11T17:03:54.458Z", + "updated_at": "2020-12-11T17:03:54.458Z", + "metadata": null + } + ], + "discounts": [ + { + "id": "disc_OpYQN4H8MOWHN2en", + "code": "CCC3C4LL88", + "is_dynamic": true, + "discount_rule_id": "dru_MDyr3lJLa00uxOsY", + "parent_discount_id": "disc_ubtdlkASI7bfUj81", + "metadata": null + } + ], + "customer_id": "cus_4eThzYSuGv", + "shipping_methods": [ + { + "id": "sm_77bEapbO8tkCqw3yo1NBuCUl", + "shipping_option_id": "so_nRvwHhEdZw", + "order_id": "order_s9RojwCU2AM8RztcldM2Uof7", + "cart_id": null, + "swap_id": null, + "return_id": null, + "price": 0, + "data": { + "id": "Parcel shop", + "city": "Los Angeles", + "postal": "90046" + } + } + ], + "metadata": null, + "display_id": 50433, + "currency_code": "dkk", + "region_id": "reg_HMnixPlOicAs7aBlXuchAGxd", + "shipping_total": 0, + "discount_total": 0, + "tax_total": 3850000, + "subtotal": 154000, + "total": 4004000 } } diff --git a/lerna-debug.log b/lerna-debug.log index 8155fcd99d..df89e4d367 100644 --- a/lerna-debug.log +++ b/lerna-debug.log @@ -1,10 +1,9 @@ -0 silly argv { _: [ 'publish' ], -0 silly argv composed: 'publish', -0 silly argv lernaVersion: '3.20.2', -0 silly argv '$0': '/usr/local/bin/lerna', -0 silly argv bump: 'from-git' } -1 notice cli v3.20.2 +0 silly argv { _: [ 'bootstrap' ], +0 silly argv lernaVersion: '3.22.1', +0 silly argv '$0': '/usr/local/bin/lerna' } +1 notice cli v3.22.1 2 verbose rootPath /Users/srindom/Developer/medusa-js -3 error JSONError: Unexpected token : in JSON at position 9 while parsing near ' "name": "@medusajs/medusa"...' in packages/medusa/package.json -3 error at module.exports (/Users/srindom/Developer/medusa-js/node_modules/parse-json/index.js:26:19) -3 error at parse (/Users/srindom/Developer/medusa-js/node_modules/load-json-file/index.js:15:9) +3 info versioning independent +4 error JSONError: Unexpected token < in JSON at position 34 while parsing near '...edusa-file-spaces",<<<<<<< HEAD "vers...' in packages/medusa-file-spaces/package.json +4 error at module.exports (/Users/srindom/Developer/medusa-js/node_modules/parse-json/index.js:26:19) +4 error at parse (/Users/srindom/Developer/medusa-js/node_modules/load-json-file/index.js:15:9) diff --git a/packages/babel-preset-medusa-package/index.js b/packages/babel-preset-medusa-package/index.js index d83de9328d..387291ebba 100644 --- a/packages/babel-preset-medusa-package/index.js +++ b/packages/babel-preset-medusa-package/index.js @@ -15,7 +15,10 @@ function preset(context, options = {}) { return { presets: [r(`@babel/preset-env`)], plugins: [ - r(`@babel/plugin-proposal-class-properties`), + r(`babel-plugin-transform-typescript-metadata`), + r(`@babel/plugin-proposal-optional-chaining`), + [r(`@babel/plugin-proposal-decorators`), { legacy: true }], + [r(`@babel/plugin-proposal-class-properties`), { loose: true }], r(`@babel/plugin-transform-classes`), r(`@babel/plugin-transform-instanceof`), r(`@babel/plugin-transform-runtime`), diff --git a/packages/babel-preset-medusa-package/package.json b/packages/babel-preset-medusa-package/package.json index 3f4c2a2485..d0a1e07d22 100644 --- a/packages/babel-preset-medusa-package/package.json +++ b/packages/babel-preset-medusa-package/package.json @@ -1,6 +1,6 @@ { "name": "babel-preset-medusa-package", - "version": "1.0.1", + "version": "1.0.2-alpha.787+0646bd3", "author": "Sebastian Rindom ", "repository": { "type": "git", @@ -9,11 +9,13 @@ }, "dependencies": { "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-decorators": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.12.1", "@babel/plugin-transform-classes": "^7.12.1", "@babel/plugin-transform-instanceof": "^7.12.1", "@babel/plugin-transform-runtime": "^7.12.1", "@babel/preset-env": "^7.12.7", + "babel-plugin-transform-typescript-metadata": "^0.3.1", "core-js": "^3.7.0" }, "peerDependencies": { @@ -23,5 +25,6 @@ "main": "index.js", "engines": { "node": ">=10.14.0" - } + }, + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-cli/package.json b/packages/medusa-cli/package.json index d7c7231798..5ebde8ddbf 100644 --- a/packages/medusa-cli/package.json +++ b/packages/medusa-cli/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa-cli", - "version": "1.0.11", + "version": "1.0.12-alpha.787+0646bd3", "description": "Command Line interface for Medusa Commerce", "main": "dist/index.js", "bin": { @@ -48,5 +48,5 @@ "resolve-cwd": "^3.0.0", "yargs": "^15.3.1" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-cli/src/create-cli.js b/packages/medusa-cli/src/create-cli.js index 3a72079222..9dda50b00e 100644 --- a/packages/medusa-cli/src/create-cli.js +++ b/packages/medusa-cli/src/create-cli.js @@ -61,6 +61,22 @@ function buildLocalCommands(cli, isLocalProject) { } cli + .command({ + command: `migrations [action]`, + desc: `Migrate the database to the most recent version.`, + builder: { + action: { + demand: true, + choices: ["run", "show"], + }, + }, + handler: handlerP( + getCommandHandler(`migrate`, (args, cmd) => { + process.env.NODE_ENV = process.env.NODE_ENV || `development` + return cmd(args) + }) + ), + }) .command({ command: `develop`, desc: `Start development server. Watches file and rebuilds when something changes`, diff --git a/packages/medusa-core-utils/package.json b/packages/medusa-core-utils/package.json index 4f45379acf..1c2a462001 100644 --- a/packages/medusa-core-utils/package.json +++ b/packages/medusa-core-utils/package.json @@ -1,6 +1,6 @@ { "name": "medusa-core-utils", - "version": "1.0.11", + "version": "1.0.12-alpha.787+0646bd3", "description": "Core utils for Medusa", "main": "dist/index.js", "repository": { @@ -30,8 +30,8 @@ "prettier": "^1.19.1" }, "dependencies": { - "joi": "^17.2.1", + "joi": "^17.3.0", "joi-objectid": "^3.0.1" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-core-utils/src/validator.js b/packages/medusa-core-utils/src/validator.js index 1623bd44ce..cc22cdafa1 100644 --- a/packages/medusa-core-utils/src/validator.js +++ b/packages/medusa-core-utils/src/validator.js @@ -7,13 +7,13 @@ Joi.address = () => { first_name: Joi.string().required(), last_name: Joi.string().required(), address_1: Joi.string().required(), - address_2: Joi.string().allow(""), + address_2: Joi.string().allow(null), city: Joi.string().required(), country_code: Joi.string().required(), - province: Joi.string().allow(""), + province: Joi.string().allow(null), postal_code: Joi.string().required(), phone: Joi.string().optional(), - metadata: Joi.object(), + metadata: Joi.object().allow(null), }) } diff --git a/packages/medusa-core-utils/yarn.lock b/packages/medusa-core-utils/yarn.lock index 876f983bc0..1bb7ac4e53 100644 --- a/packages/medusa-core-utils/yarn.lock +++ b/packages/medusa-core-utils/yarn.lock @@ -941,28 +941,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@hapi/address@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" - integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@hapi/formula@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128" - integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A== - "@hapi/hoek@^9.0.0": version "9.1.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== -"@hapi/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df" - integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw== - "@hapi/topo@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" @@ -1153,6 +1136,23 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@sideway/address@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d" + integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" + integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sinonjs/commons@^1.7.0": version "1.7.2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" @@ -3241,16 +3241,16 @@ joi-objectid@^3.0.1: resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== -joi@^17.2.1: - version "17.2.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a" - integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA== +joi@^17.3.0: + version "17.3.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2" + integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg== dependencies: - "@hapi/address" "^4.1.0" - "@hapi/formula" "^2.0.0" "@hapi/hoek" "^9.0.0" - "@hapi/pinpoint" "^2.0.0" "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.0" + "@sideway/formula" "^3.0.0" + "@sideway/pinpoint" "^2.0.0" "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" diff --git a/packages/medusa-file-spaces/package.json b/packages/medusa-file-spaces/package.json index 0889ac55a8..0769fca452 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.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "Digital Ocean Spaces file connector for Medusa", "main": "index.js", "repository": { @@ -40,9 +40,9 @@ "aws-sdk": "^2.710.0", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "stripe": "^8.50.0" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-fulfillment-manual/package.json b/packages/medusa-fulfillment-manual/package.json index afc6af3215..b76abe5e97 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.0.12", + "version": "1.0.12-alpha.208+0646bd3", "description": "A manual fulfillment provider for Medusa", "main": "index.js", "repository": { @@ -36,7 +36,7 @@ "@babel/plugin-transform-instanceof": "^7.8.3", "@babel/runtime": "^7.7.6", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js b/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js index d9e3ea5164..389dca5127 100644 --- a/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js +++ b/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js @@ -36,6 +36,11 @@ class ManualFulfillmentService extends FulfillmentService { return Promise.resolve({}) } + createFulfillment() { + // No data is being sent anywhere + return Promise.resolve({}) + } + cancelFulfillment() { return Promise.resolve({}) } diff --git a/packages/medusa-fulfillment-webshipper/package.json b/packages/medusa-fulfillment-webshipper/package.json index de8ecde80b..24d417a55d 100644 --- a/packages/medusa-fulfillment-webshipper/package.json +++ b/packages/medusa-fulfillment-webshipper/package.json @@ -1,6 +1,6 @@ { "name": "medusa-fulfillment-webshipper", - "version": "1.0.11", + "version": "1.0.10-alpha.166+0646bd3", "description": "Webshipper Fulfillment provider for Medusa", "main": "index.js", "repository": { @@ -36,7 +36,7 @@ "body-parser": "^1.19.0", "cors": "^2.8.5", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js b/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js index 3ae5682334..b4f688fea4 100644 --- a/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js +++ b/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js @@ -7,10 +7,18 @@ class WebshipperFulfillmentService extends FulfillmentService { constructor({ logger, swapService, orderService }, options) { super() - this.logger_ = logger - this.orderService_ = orderService this.options_ = options + + /** @private @const {logger} */ + this.logger_ = logger + + /** @private @const {OrderService} */ + this.orderService_ = orderService + + /** @private @const {SwapService} */ this.swapService_ = swapService + + /** @private @const {AxiosClient} */ this.client_ = new Webshipper({ account: this.options_.account, token: this.options_.api_token, @@ -38,15 +46,19 @@ class WebshipperFulfillmentService extends FulfillmentService { })) } - async validateFulfillmentData(data, _) { - if (data.require_drop_point) { + async validateFulfillmentData(optionData, data, _) { + if (optionData.require_drop_point) { if (!data.drop_point_id) { throw new Error("Must have drop point id") } else { // TODO: validate that the drop point exists } } - return data + + return { + ...optionData, + ...data, + } } async validateOption(data) { @@ -69,7 +81,22 @@ class WebshipperFulfillmentService extends FulfillmentService { * Creates a return shipment in webshipper using the given method data, and * return lines. */ - async createReturn(methodData, returnLines, fromOrder) { + async createReturn(returnOrder) { + let fromOrder + if (returnOrder.order_id) { + fromOrder = await this.orderService_.retrieve(returnOrder.order_id, { + select: ["total"], + relations: ["shipping_address", "returns"], + }) + } else if (returnOrder.swap) { + fromOrder = await this.orderService_.retrieve(returnOrder.swap.order_id, { + select: ["total"], + relations: ["shipping_address", "returns"], + }) + } + + const methodData = returnOrder.shipping_method.data + const relationships = { shipping_rate: { data: { @@ -79,7 +106,8 @@ class WebshipperFulfillmentService extends FulfillmentService { }, } - const existing = fromOrder.metadata.webshipper_order_id + const existing = + fromOrder.metadata && fromOrder.metadata.webshipper_order_id if (existing) { relationships.order = { data: { @@ -93,8 +121,9 @@ class WebshipperFulfillmentService extends FulfillmentService { if (this.invoiceGenerator_) { const base64Invoice = await this.invoiceGenerator_.createReturnInvoice( fromOrder, - returnLines + returnOrder.items ) + docs.push({ document_size: "A4", document_format: "PDF", @@ -108,7 +137,7 @@ class WebshipperFulfillmentService extends FulfillmentService { type: "shipments", attributes: { reference: `R${fromOrder.display_id}-${fromOrder.returns.length + 1}`, - ext_ref: `${fromOrder._id}.${fromOrder.returns.length}`, + ext_ref: `${fromOrder.id}.${returnOrder.id}`, is_return: true, included_documents: docs, packages: [ @@ -121,21 +150,20 @@ class WebshipperFulfillmentService extends FulfillmentService { width: 15, length: 15, }, - customs_lines: returnLines.map((item) => { + customs_lines: returnOrder.items.map(({ item, quantity }) => { return { - ext_ref: item._id, - sku: item.content.variant.sku, + ext_ref: item.id, + sku: item.variant.sku, description: item.title, - quantity: item.quantity, + quantity: quantity, country_of_origin: - item.content.variant.metadata && - item.content.variant.metadata.origin_country, + item.variant.origin_country || + item.variant.product.origin_country, tarif_number: - item.content.variant.metadata && - item.content.variant.metadata.hs_code, - unit_price: item.content.unit_price, - vat_percent: fromOrder.tax_rate * 100, - currency: fromOrder.currency_code, + item.variant.hs_code || item.variant.product.hs_code, + unit_price: item.unit_price / 100, + vat_percent: fromOrder.tax_rate, + currency: fromOrder.currency_code.toUpperCase(), } }), }, @@ -148,7 +176,7 @@ class WebshipperFulfillmentService extends FulfillmentService { address_2: shipping_address.address_2, zip: shipping_address.postal_code, city: shipping_address.city, - country_code: shipping_address.country_code, + country_code: shipping_address.country_code.toUpperCase(), state: shipping_address.province, phone: shipping_address.phone, email: fromOrder.email, @@ -196,7 +224,12 @@ class WebshipperFulfillmentService extends FulfillmentService { return toReturn } - async createOrder(methodData, fulfillmentItems, fromOrder) { + async createFulfillment( + methodData, + fulfillmentItems, + fromOrder, + fulfillment + ) { const existing = fromOrder.metadata && fromOrder.metadata.webshipper_order_id @@ -228,13 +261,12 @@ class WebshipperFulfillmentService extends FulfillmentService { }) } - let visible_ref = `${fromOrder.display_id}-${ - fromOrder.fulfillments.length + 1 - }` - let ext_ref = `${fromOrder._id}.${fromOrder.fulfillments.length}` + let id = fulfillment.id + let visible_ref = `${fromOrder.display_id}-${id.substr(id.length - 4)}` + let ext_ref = `${fromOrder.id}.${fulfillment.id}` if (fromOrder.is_swap) { - ext_ref = `S${fromOrder._id}.${fromOrder.fulfillments.length}` + ext_ref = `${fromOrder.id}.${fulfillment.id}` visible_ref = `S-${fromOrder.display_id}` } @@ -247,17 +279,17 @@ class WebshipperFulfillmentService extends FulfillmentService { visible_ref, order_lines: fulfillmentItems.map((item) => { return { - ext_ref: item._id, - sku: item.content.variant.sku, + ext_ref: item.id, + sku: item.variant.sku, description: item.title, quantity: item.quantity, country_of_origin: - item.content.variant.metadata && - item.content.variant.metadata.origin_country, + item.variant.origin_country || + item.variant.product.origin_country, tarif_number: - item.content.variant.metadata && - item.content.variant.metadata.hs_code, - unit_price: item.content.unit_price, + item.variant.hs_code || item.variant.product.hs_code, + unit_price: item.unit_price / 100, + vat_percent: fromOrder.tax_rate, } }), delivery_address: { @@ -266,12 +298,12 @@ class WebshipperFulfillmentService extends FulfillmentService { address_2: shipping_address.address_2, zip: shipping_address.postal_code, city: shipping_address.city, - country_code: shipping_address.country_code, + country_code: shipping_address.country_code.toUpperCase(), state: shipping_address.province, phone: shipping_address.phone, email: fromOrder.email, }, - currency: fromOrder.currency_code, + currency: fromOrder.currency_code.toUpperCase(), }, relationships: { order_channel: { @@ -296,7 +328,7 @@ class WebshipperFulfillmentService extends FulfillmentService { zip: methodData.drop_point_zip, address_1: methodData.drop_point_address_1, city: methodData.drop_point_city, - country_code: methodData.drop_point_country_code, + country_code: methodData.drop_point_country_code.toUpperCase(), } } if (invoice) { @@ -326,7 +358,7 @@ class WebshipperFulfillmentService extends FulfillmentService { const wsOrder = await this.retrieveRelationship( body.data.relationships.order ) - if (wsOrder.data.attributes.ext_ref) { + if (wsOrder.data && wsOrder.data.attributes.ext_ref) { const trackingNumbers = body.data.attributes.tracking_links.map( (l) => l.number ) @@ -334,23 +366,44 @@ class WebshipperFulfillmentService extends FulfillmentService { "." ) - if (orderId.charAt(0) === "S") { - const swap = await this.swapService_.retrieve(orderId.substring(1)) - const fulfillment = swap.fulfillments[fulfillmentIndex] - await this.swapService_.createShipment( - swap._id, - fulfillment._id, - trackingNumbers - ) - } else { - const order = await this.orderService_.retrieve(orderId) - const fulfillment = order.fulfillments[fulfillmentIndex] - if (fulfillment) { - await this.orderService_.createShipment( - order._id, - fulfillment._id, + if (orderId.charAt(0).toLowerCase() === "s") { + if (fulfillmentIndex.startsWith("ful")) { + return this.swapService_.createShipment( + orderId, + fulfillmentIndex, trackingNumbers ) + } else { + const swap = await this.swapService_.retrieve(orderId.substring(1), { + relations: ["fulfillments"], + }) + const fulfillment = swap.fulfillments[fulfillmentIndex] + return this.swapService_.createShipment( + swap.id, + fulfillment.id, + trackingNumbers + ) + } + } else { + if (fulfillmentIndex.startsWith("ful")) { + return this.orderService_.createShipment( + orderId, + fulfillmentIndex, + trackingNumbers + ) + } else { + const order = await this.orderService_.retrieve(orderId, { + relations: ["fulfillments"], + }) + + const fulfillment = order.fulfillments[fulfillmentIndex] + if (fulfillment) { + return this.orderService_.createShipment( + order.id, + fulfillment.id, + trackingNumbers + ) + } } } } @@ -392,7 +445,7 @@ class WebshipperFulfillmentService extends FulfillmentService { shipping_rate_id: id, delivery_address: { zip, - country_code: countryCode, + country_code: countryCode.toUpperCase(), address_1: address1, }, }, diff --git a/packages/medusa-interfaces/package.json b/packages/medusa-interfaces/package.json index d6bebb8e66..c0ad352120 100644 --- a/packages/medusa-interfaces/package.json +++ b/packages/medusa-interfaces/package.json @@ -1,6 +1,6 @@ { "name": "medusa-interfaces", - "version": "1.0.14", + "version": "1.0.14-alpha.170+0646bd3", "description": "Core interfaces for Medusa", "main": "dist/index.js", "repository": { @@ -28,13 +28,14 @@ "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.2", - "prettier": "^1.19.1" + "prettier": "^1.19.1", + "typeorm": "^0.2.29" }, "peerDependencies": { - "mongoose": "5.x" + "typeorm": "0.x" }, "dependencies": { - "medusa-core-utils": "^1.0.11" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-interfaces/src/__tests__/base-service.js b/packages/medusa-interfaces/src/__tests__/base-service.js index 76b5055c1f..5961237b91 100644 --- a/packages/medusa-interfaces/src/__tests__/base-service.js +++ b/packages/medusa-interfaces/src/__tests__/base-service.js @@ -1,6 +1,39 @@ import BaseService from "../base-service" +import { In, Not } from "typeorm" describe("BaseService", () => { + describe("buildQuery_", () => { + const baseService = new BaseService() + + it("successfully creates query", () => { + const q = baseService.buildQuery_( + { + id: "1234", + test1: ["123", "12", "1"], + test2: Not("this"), + rec: { + first: ["1", "2", "3"], + }, + }, + { + relations: ["1234"], + } + ) + + expect(q).toEqual({ + where: { + id: "1234", + test1: In(["123", "12", "1"]), + test2: Not("this"), + rec: { + first: In(["1", "2", "3"]), + }, + }, + relations: ["1234"], + }) + }) + }) + describe("addDecorator", () => { const baseService = new BaseService() diff --git a/packages/medusa-interfaces/src/base-service.js b/packages/medusa-interfaces/src/base-service.js index b9117099d5..80685d413f 100644 --- a/packages/medusa-interfaces/src/base-service.js +++ b/packages/medusa-interfaces/src/base-service.js @@ -1,3 +1,6 @@ +import { MedusaError } from "medusa-core-utils" +import { In, FindOperator, getManager } from "typeorm" + /** * Common functionality for Services * @interface @@ -7,6 +10,179 @@ class BaseService { this.decorators_ = [] } + withTransaction() { + console.log("WARN: withTransaction called without custom implementation") + return this + } + + /** + * Used to build TypeORM queries. + */ + buildQuery_(selector, config = {}) { + const build = obj => { + const where = Object.entries(obj).reduce((acc, [key, value]) => { + switch (true) { + case value instanceof FindOperator: + acc[key] = value + break + case Array.isArray(value): + acc[key] = In([...value]) + break + case value !== null && typeof value === "object": + acc[key] = build(value) + break + default: + acc[key] = value + break + } + + return acc + }, {}) + + return where + } + + const query = { + where: build(selector), + } + + if ("skip" in config) { + query.skip = config.skip + } + + if ("take" in config) { + query.take = config.take + } + + if ("relations" in config) { + query.relations = config.relations + } + + if ("select" in config) { + query.select = config.select + } + + if ("order" in config) { + query.order = config.order + } + + return query + } + + /** + * Confirms whether a given raw id is valid. Fails if the provided + * id is null or undefined. The validate function takes an optional config + * param, to support checking id prefix and length. + * @param {string} rawId - the id to validate. + * @param {object?} config - optional config + * @returns {string} the rawId given that nothing failed + */ + validateId_(rawId, config = {}) { + const { prefix, length } = config + if (!rawId) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to validate id: ${rawId}` + ) + } + + if (prefix || length) { + const [pre, rand] = rawId.split("_") + if (prefix && pre !== prefix) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}` + ) + } + + if (length && length !== rand.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The provided id: ${rawId} does not adhere to length constraint: ${length}` + ) + } + } + + return rawId + } + + shouldRetryTransaction(err) { + const code = typeof err === "object" ? String(err.code) : null + return code === "40001" || code === "40P01" + } + + /** + * Wraps some work within a transactional block. If the service already has + * a transaction manager attached this will be reused, otherwise a new + * transaction manager is created. + * @param {function} work - the transactional work to be done + * @param {string} isolation - the isolation level to be used for the work. + * @return {any} the result of the transactional work + */ + async atomicPhase_(work, isolation) { + if (this.transactionManager_) { + return work(this.transactionManager_) + } else { + const temp = this.manager_ + const doWork = async m => { + this.manager_ = m + this.transactionManager_ = m + try { + const result = await work(m) + this.manager_ = temp + this.transactionManager_ = undefined + return result + } catch (error) { + this.manager_ = temp + this.transactionManager_ = undefined + throw error + } + } + + if (isolation) { + let result + try { + result = await this.manager_.transaction(isolation, m => doWork(m)) + return result + } catch (error) { + if (this.shouldRetryTransaction(error)) { + return this.manager_.transaction(isolation, m => doWork(m)) + } else { + throw error + } + } + } + return this.manager_.transaction(m => doWork(m)) + } + } + + /** + * Dedicated method to set metadata. + * @param {string} obj - the entity to apply metadata to. + * @param {object} metadata - the metadata to set + * @return {Promise} resolves to the updated result. + */ + setMetadata_(obj, metadata) { + const existing = obj.metadata || {} + const newData = {} + for (const [key, value] of Object.entries(metadata)) { + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } + newData[key] = value + } + + const updated = { + ...existing, + ...newData, + } + + return updated + } + /** * Adds a decorator to a service. The decorator must be a function and should * return a decorated object. diff --git a/packages/medusa-interfaces/src/fulfillment-service.js b/packages/medusa-interfaces/src/fulfillment-service.js index c5d3f2bf51..4e56063a63 100644 --- a/packages/medusa-interfaces/src/fulfillment-service.js +++ b/packages/medusa-interfaces/src/fulfillment-service.js @@ -60,7 +60,7 @@ class BaseFulfillmentService extends BaseService { throw Error("calculatePrice must be overridden by the child class") } - createOrder() { + createFulfillment() { throw Error("createOrder must be overridden by the child class") } diff --git a/packages/medusa-payment-adyen/package.json b/packages/medusa-payment-adyen/package.json index 6f2a1de0e6..8d2fd82c24 100644 --- a/packages/medusa-payment-adyen/package.json +++ b/packages/medusa-payment-adyen/package.json @@ -1,6 +1,6 @@ { "name": "medusa-payment-adyen", - "version": "1.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "Adyen Payment provider for Medusa Commerce", "main": "index.js", "repository": { @@ -24,7 +24,7 @@ "cross-env": "^7.0.2", "eslint": "^6.8.0", "jest": "^25.5.2", - "medusa-test-utils": "^1.0.13" + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, "scripts": { "build": "babel src -d .", @@ -42,7 +42,7 @@ "body-parser": "^1.19.0", "cors": "^2.8.5", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-payment-adyen/src/api/routes/hooks/adyen-notification.js b/packages/medusa-payment-adyen/src/api/routes/hooks/adyen-notification.js new file mode 100644 index 0000000000..259bb74270 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/hooks/adyen-notification.js @@ -0,0 +1,17 @@ +export default async (req, res) => { + const adyenService = req.scope.resolve("adyenService") + const eventBus = req.scope.resolve("eventBusService") + + const notification = req.body + const event = notification.notificationItems[0].NotificationRequestItem + + const valid = adyenService.validateNotification(event) + + if (!valid) { + res.status(401).send(`Unauthorized webhook event`) + return + } + + eventBus.emit("adyen.notification_received", event) + res.status(200).send("[accepted]") +} diff --git a/packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js b/packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js deleted file mode 100644 index 2fc9884bfa..0000000000 --- a/packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js +++ /dev/null @@ -1,41 +0,0 @@ -export default async (req, res) => { - const adyenService = req.scope.resolve("adyenService") - - const notification = req.body - const event = notification.notificationItems[0].NotificationRequestItem - - const valid = adyenService.validateNotification(event) - - if (!valid) { - res.status(401).send(`Unauthorized webhook event`) - return - } - - if (event.success === "true" && event.eventCode === "AUTHORISATION") { - const orderService = req.scope.resolve("orderService") - const cartService = req.scope.resolve("cartService") - - const cartId = event.additionalData["metadata.cart_id"] - - try { - const order = await orderService.retrieveByCartId(cartId) - console.log(order) - } catch (error) { - const cart = await cartService.retrieve(cartId) - await orderService.createFromCart(cart) - } - - // Create from cart - res.status(200).send("[accepted]") - return - } - - if (event.success === "true" && event.eventCode === "CAPTURE") { - // Create from cart - console.log("Captured") - res.status(200).send("[accepted]") - return - } - - res.status(200).send("[accepted]") -} diff --git a/packages/medusa-payment-adyen/src/api/routes/hooks/index.js b/packages/medusa-payment-adyen/src/api/routes/hooks/index.js index fff3b71eb9..807fec9612 100644 --- a/packages/medusa-payment-adyen/src/api/routes/hooks/index.js +++ b/packages/medusa-payment-adyen/src/api/routes/hooks/index.js @@ -18,12 +18,12 @@ export default (app, rootDirectory) => { }) ) - app.use("/adyen-hooks", route) + app.use("/adyen/webhooks", route) route.post( - "/capture", + "/notification", bodyParser.json(), - middlewares.wrap(require("./capture-hook").default) + middlewares.wrap(require("./adyen-notification").default) ) return app diff --git a/packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js b/packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js deleted file mode 100644 index d2fa652b04..0000000000 --- a/packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Validator, MedusaError } from "medusa-core-utils" - -export default async (req, res) => { - const schema = Validator.object().keys({ - cart_id: Validator.string().required(), - provider_id: Validator.string().required(), - payment_data: Validator.object().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const cartService = req.scope.resolve("cartService") - const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`) - - const cart = await cartService.retrieve(value.cart_id) - - const { data } = await paymentProvider.authorizePayment( - cart, - value.payment_data.paymentMethod - ) - - const transactionReference = data.pspReference - - let newPaymentSession = cart.payment_sessions.find( - (ps) => ps.provider_id === value.provider_id - ) - - newPaymentSession = { - ...newPaymentSession, - data, - } - - await cartService.setMetadata( - cart._id, - "adyen_transaction_ref", - transactionReference - ) - - await cartService.updatePaymentSession( - cart._id, - value.provider_id, - newPaymentSession - ) - - await cartService.setPaymentMethod(cart._id, { - provider_id: value.provider_id, - data, - }) - - res.status(200).json({ data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js b/packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js deleted file mode 100644 index 768e3c6537..0000000000 --- a/packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Validator, MedusaError } from "medusa-core-utils" - -export default async (req, res) => { - const schema = Validator.object().keys({ - payload: Validator.string().required(), - payment_data: Validator.string().required(), - provider_id: Validator.string().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const adyen = req.scope.resolve("adyenService") - const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`) - - const { data } = await adyen.checkPaymentResult( - value.payment_data, - value.payload - ) - - const status = await paymentProvider.getStatus(data) - - res.status(200).json({ status }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa-payment-adyen/src/api/routes/store/index.js b/packages/medusa-payment-adyen/src/api/routes/store/index.js index 79b54fe7f6..4dbb57ba25 100644 --- a/packages/medusa-payment-adyen/src/api/routes/store/index.js +++ b/packages/medusa-payment-adyen/src/api/routes/store/index.js @@ -26,29 +26,5 @@ export default (app, rootDirectory) => { middlewares.wrap(require("./retrieve-payment-methods").default) ) - route.post( - "/authorize", - bodyParser.json(), - middlewares.wrap(require("./authorize-payment").default) - ) - - route.post( - "/update", - bodyParser.json(), - middlewares.wrap(require("./update-payment").default) - ) - - route.post( - "/payment-status", - bodyParser.json(), - middlewares.wrap(require("./check-payment-status").default) - ) - - route.get( - "/payment-status", - bodyParser.json(), - middlewares.wrap(require("./check-payment-status").default) - ) - return app } diff --git a/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js b/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js index 1346270d88..8887f2b8e0 100644 --- a/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js +++ b/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js @@ -13,39 +13,44 @@ export default async (req, res) => { try { const adyenService = req.scope.resolve("adyenService") const cartService = req.scope.resolve("cartService") - const regionService = req.scope.resolve("regionService") - const totalsService = req.scope.resolve("totalsService") - const cart = await cartService.retrieve(value.cart_id) - const region = await regionService.retrieve(cart.region_id) - const total = await totalsService.getTotal(cart) + const cart = await cartService.retrieve(value.cart_id, { + select: ["total"], + relations: ["region", "region.payment_providers", "payment_sessions"], + }) - const allowedMethods = cart.payment_sessions.map( - (ps) => ps.provider_id.split("Adyen")[0] - ) + const allowedMethods = cart.payment_sessions.map((ps) => { + if (ps.provider_id.includes("adyen")) { + return ps.provider_id.split("-adyen")[0] + } + }) if (allowedMethods.length === 0) { res.status(200).json({ paymentMethods: {} }) return } - const { data } = await adyenService.retrievePaymentMethods( - cart, + const pmMethods = await adyenService.retrievePaymentMethods( allowedMethods, - total, - region.currency_code + cart.total, + cart.currency_code, + cart.customer_id || "" ) // Adyen does not behave 100% correctly in regards to allowed methods // Therefore, we sanity filter before sending them to the storefront - const { paymentMethods, groups } = data + const { paymentMethods, groups, storedPaymentMethods } = pmMethods const methods = paymentMethods.filter((pm) => allowedMethods.includes(pm.type) ) - res - .status(200) - .json({ paymentMethods: { paymentMethods: methods, groups } }) + const response = { + paymentMethods: methods, + groups, + storedPaymentMethods, + } + + res.status(200).json({ payment_methods: response }) } catch (err) { throw err } diff --git a/packages/medusa-payment-adyen/src/api/routes/store/update-payment.js b/packages/medusa-payment-adyen/src/api/routes/store/update-payment.js deleted file mode 100644 index 219a8d3e32..0000000000 --- a/packages/medusa-payment-adyen/src/api/routes/store/update-payment.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Validator, MedusaError } from "medusa-core-utils" - -export default async (req, res) => { - const schema = Validator.object().keys({ - cart_id: Validator.string().required(), - provider_id: Validator.string().required(), - payment_data: Validator.object().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const cartService = req.scope.resolve("cartService") - const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`) - - const cart = await cartService.retrieve(value.cart_id) - - const { data } = await paymentProvider.updatePayment( - value.payment_data.paymentData, - value.payment_data.details - ) - - await cartService.updatePaymentSession(cart._id, value.provider_id, data) - - res.status(200).json({ data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa-payment-adyen/src/services/adyen.js b/packages/medusa-payment-adyen/src/services/adyen.js index d90b51e492..4310536964 100644 --- a/packages/medusa-payment-adyen/src/services/adyen.js +++ b/packages/medusa-payment-adyen/src/services/adyen.js @@ -2,72 +2,141 @@ import axios from "axios" import _ from "lodash" import { hmacValidator } from "@adyen/api-library" import { BaseService } from "medusa-interfaces" +import { Client, Config, CheckoutAPI } from "@adyen/api-library" class AdyenService extends BaseService { - constructor({ regionService, cartService, totalsService }, options) { + constructor({ cartService }, options) { super() - this.regionService_ = regionService - + /** @private @constant {CartService} */ this.cartService_ = cartService - this.totalsService_ = totalsService - + /** + * { + * api_key: "", + * notification_hmac: "", + * return_url: "", + * merchant_account: "", + * origin: "", + * environment: "", + * live_endpoint_prefix: "", + * payment_endpoint: "" + * } + */ this.options_ = options - this.adyenCheckoutApi = axios.create({ - baseURL: "https://checkout-test.adyen.com/v53", - headers: { - "Content-Type": "application/json", - "x-API-key": this.options_.api_key, - }, + /** @private @constant {AxiosClient} */ + this.adyenClient_ = this.initAdyenClient() + + /** @private @constant {AdyenClient} */ + this.adyenPaymentApi = this.initPaymentClient() + } + + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new AdyenService({ + cartService: this.cartService_, + totalsService: this.totalsService_, }) - this.adyenPaymentApi = axios.create({ - baseURL: "https://pal-test.adyen.com/pal/servlet/Payment/v52", - headers: { - "Content-Type": "application/json", - "x-API-key": this.options_.api_key, - }, - }) + this.transactionManager_ = transactionManager + + return cloned } getOptions() { return this.options_ } - validateNotification(event) { + initPaymentClient() { + return axios.create({ + baseURL: this.options_.payment_endpoint, + headers: { + "Content-Type": "application/json", + "x-API-key": this.options_.api_key, + }, + }) + } + + initAdyenClient() { + const config = new Config() + config.apiKey = this.options_.api_key + config.merchantAccount = this.options_.merchant_account + + const client = new Client({ + config, + }) + + client.setEnvironment( + this.options_.environment, + this.options_.live_endpoint_prefix + ) + + return client + } + + /** + * Validates an Adyen webhook notification + * @param {object} notification - notification to validate + * @returns {string} the status of the payment + */ + validateNotification(notification) { const validator = new hmacValidator() + const validated = validator.validateHMAC( - event, + notification, this.options_.notification_hmac ) return validated } /** - * Retrieve payment methods from Ayden using country as filter. - * @param {string} countryCode - country code of cart - * @param {string} shopperLocale - locale used on website - * @returns {string} the status of the payment + * Retrieve stored payment methods from Adyen. + * @param {Customer} customer - customer to retrieve methods for + * @returns {Promise} result containing the stored payment methods from Adyen */ - async retrievePaymentMethods(cart, allowedMethods, total, currency) { + async retrieveSavedMethods(customer) { let request = { - allowedPaymentMethods: allowedMethods, - amount: { - value: total * 100, - currency: currency, - }, merchantAccount: this.options_.merchant_account, - channel: this.options_.channel, - } - - if (cart.customer_id) { - request.shopperReference = cart.customer_id + channel: "Web", + shopperReference: customer.id, } try { - return await this.adyenCheckoutApi.post("/paymentMethods", request) + const checkout = new CheckoutAPI(this.adyenClient_) + const methods = await checkout.paymentMethods(request) + return methods.storedPaymentMethods || [] + } catch (error) { + throw error + } + } + + /** + * Retrieve payment methods from Adyen. + * @param {string[]} allowedMethods - the allowed methods based on region + * @param {number} total - total amount to be paid with payment methods + * @param {string} currency - currency code to use for the payment + * @param {string} customerId - id of the customer paying + * @returns {Promise} result containing the payment methods from Adyen + */ + async retrievePaymentMethods(allowedMethods, total, currency, customerId) { + let request = { + allowedPaymentMethods: allowedMethods, + amount: { + value: total, + currency: currency, + }, + merchantAccount: this.options_.merchant_account, + channel: "Web", + shopperReference: customerId, + } + + try { + const checkout = new CheckoutAPI(this.adyenClient_) + return checkout.paymentMethods(request) } catch (error) { throw error } @@ -75,89 +144,218 @@ class AdyenService extends BaseService { /** * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart + * @param {object} paymentData - payment method data from cart * @returns {string} the status of the payment */ - async getStatus(_) { - let status = "initial" + getStatus(paymentData) { + const { resultCode } = paymentData + let status = "pending" + + if (resultCode === "Pending") { + return status + } + + if (resultCode === "Refused") { + return status + } + + if (resultCode === "Error") { + status = "error" + } + + if (resultCode === "Authorised") { + status = "authorized" + } + + if (resultCode === "Canceled") { + status = "canceled" + } + + if (resultCode === "ChallengeShopper") { + status = "requires_more" + } + + if (resultCode === "RedirectShopper") { + status = "requires_more" + } + + if (resultCode === "IdentifyShopper") { + status = "requires_more" + } + return status } /** * Creates Adyen payment object. - * @param {any} _ - placeholder object - * @returns {Object} empty payment data + * @param {Cart} cart - cart to initiate payment for + * @returns {object} empty payment data */ - async createPayment(_) { - return {} - } - - async retrievePayment(data) { - return data - } - - async updatePayment(paymentData, details) { - const request = { - paymentData, - details, - } - return this.adyenCheckoutApi.post("/payments/details", request) + async createPayment(cart) { + return { cart_id: cart.id } } /** - * Creates and authorizes an Ayden payment - * @returns {Object} payment data result + * Retrieves Adyen payment. This is not supported by adyen, so we simply + * return the current payment method data + * @param {object} data - payment session + * @returns {object} payment method data */ - async authorizePayment(cart, paymentMethod) { - const region = await this.regionService_.retrieve(cart.region_id) - const total = await this.totalsService_.getTotal(cart) + async getPaymentData(paymentSession) { + return { ...paymentSession.data } + } + + /** + * Retrieves Adyen payment. This is not supported by adyen, so we simply + * return the current payment method data + * @param {object} sessionData - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent + */ + async retrieve(sessionData) { + return sessionData + } + + /** + * Creates and authorizes an Adyen payment. + * Requires cart_id in context for authorization. + * Return status of authorization result. + * @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(session, context) { + const sessionData = session.data + + const status = this.getStatus(sessionData) + + // If session data is present, we already called authorize once. + // Therefore, this is most likely a call for getting additional details + if (status === "requires_more") { + const updated = await this.updatePaymentData(sessionData, { + details: sessionData.details, + paymentData: sessionData.paymentData, + }) + + return { data: updated, status: this.getStatus(updated) } + } + + if (status === "authorized") { + return { data: sessionData, status: "authorized" } + } + + const cart = await this.cartService_.retrieve(session.cart_id, { + select: ["total"], + relations: ["region", "shipping_address"], + }) + + const amount = { + currency: cart.region.currency_code.toUpperCase(), + value: cart.total, + } let request = { - amount: { - currency: region.currency_code, - value: total * 100, - }, + amount, + shopperIP: context.ip_address || "", shopperReference: cart.customer_id, - paymentMethod, - reference: cart._id, + paymentMethod: sessionData.paymentData.paymentMethod, + reference: cart.id, merchantAccount: this.options_.merchant_account, returnUrl: this.options_.return_url, + origin: this.options_.origin, + channel: "Web", + redirectFromIssuerMethod: "GET", + browserInfo: sessionData.browserInfo || {}, + billingAddress: { + city: cart.shipping_address.city, + country: cart.shipping_address.country_code, + houseNumberOrName: cart.shipping_address.address_2 || "", + postalCode: cart.shipping_address.postal_code, + stateOrProvice: cart.shipping_address.province || "", + street: cart.shipping_address.address_1, + }, metadata: { - cart_id: cart._id, + cart_id: cart.id, }, } - if (paymentMethod.storedPaymentMethodId) { + // If customer chose to save the payment method + if (sessionData.storePaymentMethod) { + request.storePaymentMethod = "true" request.shopperInteraction = "Ecommerce" request.recurringProcessingModel = "CardOnFile" } - return await this.adyenCheckoutApi.post("/payments", request) + const checkout = new CheckoutAPI(this.adyenClient_) + + try { + const authorizedPayment = await checkout.payments(request, { + idempotencyKey: context.idempotency_key || "", + }) + + return { + data: authorizedPayment, + status: this.getStatus(authorizedPayment), + } + } catch (error) { + throw error + } } - async checkPaymentResult(paymentData, payload) { - const request = { - paymentData, - details: { - payload, - }, + async updatePaymentData(sessionData, update) { + if (_.isEmpty(update.details)) { + return { ...sessionData, ...update } } - return this.adyenCheckoutApi.post("/payments/details", request) + + const checkout = new CheckoutAPI(this.adyenClient_) + const updated = await checkout.paymentsDetails(update) + + return updated } /** - * Captures an Ayden payment - * @param {Object} data - payment data to capture - * @returns {Object} payment data result of capture + * Updates an Adyen payment. + * @param {object} paymentData - payment data to update + * @param {details} details - details to update + * @returns {Promise} result of the update operation */ - async capturePayment(data) { - const { pspReference, amount } = data + async updatePayment(paymentData, details) { + return paymentData + } + + /** + * Additional details + * @param {object} paymentData - payment data + * @param {object} details - payment details + * @returns {Promise} current payment result + */ + async additionalDetails(paymentData, details) { + const request = { + paymentData, + details, + } + + const checkout = new CheckoutAPI(this.adyenClient_) + return checkout.paymentsDetails(request) + } + + /** + * Captures an Adyen payment + * @param {object} payment - payment to capture + * @returns {string} status = processing_captures + */ + async capturePayment(payment) { + const { pspReference, merchantReference } = payment.data + const { amount, currency_code } = payment try { const captured = await this.adyenPaymentApi.post("/capture", { originalReference: pspReference, - modificationAmount: amount, + modificationAmount: { + value: amount, + currency: currency_code.toUpperCase(), + }, merchantAccount: this.options_.merchant_account, + reference: merchantReference, }) if ( @@ -170,40 +368,55 @@ class AdyenService extends BaseService { ) } - return captured + return { originalReference: pspReference, ...captured.data } } catch (error) { - console.log(error) throw error } } /** - * Refunds an Ayden payment - * @param {Object} paymentData - payment data to refund + * Refunds an Adyen payment + * @param {object} payment - payment to refund * @param {number} amountToRefund - amount to refund - * @returns {Object} payment data result of refund + * @returns {object} payment data result of refund */ - async refundPayment(data) { - const { pspReference, amount } = data + async refundPayment(payment, amountToRefund) { + const { originalReference, merchantReference } = payment.data + const { currency_code } = payment + + const refundAmount = { + currency: currency_code.toUpperCase(), + value: amountToRefund, + } try { - return this.adyenPaymentApi.post("/refund", { - originalReference: pspReference, + const refunded = await this.adyenPaymentApi.post("/refund", { + originalReference, merchantAccount: this.options_.merchant_account, - modificationAmount: amount, + modificationAmount: refundAmount, + reference: merchantReference, }) + + return { originalReference, ...refunded.data } } catch (error) { throw error } } /** - * Cancels an Ayden payment - * @param {Object} paymentData - payment data to cancel - * @returns {Object} payment data result of cancel + * Adyen does not have a way of deleting payments, hence the empty impl. */ - async cancelPayment(paymentData) { - const { pspReference } = paymentData + async deletePayment(_) { + return {} + } + + /** + * Cancels an Adyen payment. + * @param {object} payment - payment to cancel + * @returns {object} payment data result of cancel + */ + async cancelPayment(payment) { + const { pspReference } = payment.data try { return this.adyenPaymentApi.post("/cancel", { diff --git a/packages/medusa-payment-adyen/src/services/applepay-adyen.js b/packages/medusa-payment-adyen/src/services/applepay-adyen.js new file mode 100644 index 0000000000..d93ea5992e --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/applepay-adyen.js @@ -0,0 +1,104 @@ +import _ from "lodash" +import https from "https" +import fs from "fs" +import axios from "axios" +import { PaymentService } from "medusa-interfaces" + +class ApplePayAdyenService extends PaymentService { + static identifier = "applepay-adyen" + + constructor({ adyenService }, options) { + super() + + this.adyenService_ = adyenService + + this.options_ = options + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async getApplePaySession(validationUrl) { + let certificate + try { + // Place certificate in root folder + certificate = fs.readFileSync("./apple-pay-cert.pem") + } catch (error) { + throw new Error( + "Could not find ApplePay certificate. Make sure to place it in root folder of your server" + ) + } + + const httpsAgent = new https.Agent({ + cert: certificate, + key: certificate, + rejectUnauthorized: false, + }) + + const request = { + merchantIdentifier: this.options_.applepay_merchant_id, + displayName: this.options_.applepay_display_name, + initiative: "web", + initiativeContext: this.options_.applepay_initiative_context, + } + + return axios.post(validationUrl, request, { + httpsAgent, + }) + } + + async authorizePayment(sessionData, context) { + return this.adyenService_.authorizePayment(sessionData, context) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePaymentData(sessionData, update) { + return this.adyenService_.updatePaymentData(sessionData, update) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + return this.adyenService_.capturePayment(data) + } + + async refundPayment(data, amountToRefund) { + return this.adyenService_.refundPayment(data, amountToRefund) + } + + async cancelPayment(data) { + return this.adyenService_.cancelPayment(data) + } +} + +export default ApplePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/applepay.js b/packages/medusa-payment-adyen/src/services/applepay.js deleted file mode 100644 index 556ad3252d..0000000000 --- a/packages/medusa-payment-adyen/src/services/applepay.js +++ /dev/null @@ -1,74 +0,0 @@ -import _ from "lodash" -import { PaymentService } from "medusa-interfaces" - -class ApplePayAdyenService extends PaymentService { - static identifier = "applepayAdyen" - - constructor({ adyenService }) { - super() - - this.adyenService_ = adyenService - } - - /** - * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart - * @returns {string} the status of the payment - */ - async getStatus(paymentData) { - const { resultCode } = paymentData - let status = "initial" - - if (resultCode === "Authorised") { - status = "authorized" - } - - return status - } - - async createPayment(_) { - return {} - } - - async authorizePayment(cart, paymentMethod) { - return this.adyenService_.authorizePayment(cart, paymentMethod) - } - - async retrievePayment(data) { - return this.adyenService_.retrievePayment(data) - } - - async updatePayment(data, _) { - return this.adyenService_.updatePayment(data) - } - - async deletePayment(data) { - return this.adyenService_.deletePayment(data) - } - - async capturePayment(data) { - try { - return this.adyenService_.capturePayment(data) - } catch (error) { - throw error - } - } - - async refundPayment(data) { - try { - return this.adyenService_.refundPayment(data) - } catch (error) { - throw error - } - } - - async cancelPayment(data) { - try { - return this.adyenService_.cancelPayment(data) - } catch (error) { - throw error - } - } -} - -export default ApplePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/card-adyen.js b/packages/medusa-payment-adyen/src/services/card-adyen.js new file mode 100644 index 0000000000..bc6028dda9 --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/card-adyen.js @@ -0,0 +1,62 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class CardAdyenService extends PaymentService { + static identifier = "scheme-adyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + async retrieveSavedMethods(customer) { + return this.adyenService_.retrieveSavedMethods(customer) + } + + async getStatus(paymentData) { + return this.adyenService_.getStatus(paymentData) + } + + async createPayment(data) { + return this.adyenService_.createPayment(data) + } + + async authorizePayment(sessionData, context) { + return this.adyenService_.authorizePayment(sessionData, context) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async updatePaymentData(sessionData, update) { + return this.adyenService_.updatePaymentData(sessionData, update) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + return this.adyenService_.capturePayment(data) + } + + async refundPayment(data, amountToRefund) { + return this.adyenService_.refundPayment(data, amountToRefund) + } + + async cancelPayment(data) { + return this.adyenService_.cancelPayment(data) + } +} + +export default CardAdyenService diff --git a/packages/medusa-payment-adyen/src/services/card.js b/packages/medusa-payment-adyen/src/services/card.js deleted file mode 100644 index 3c6508d528..0000000000 --- a/packages/medusa-payment-adyen/src/services/card.js +++ /dev/null @@ -1,74 +0,0 @@ -import _ from "lodash" -import { PaymentService } from "medusa-interfaces" - -class CardAdyenService extends PaymentService { - static identifier = "schemeAdyen" - - constructor({ adyenService }) { - super() - - this.adyenService_ = adyenService - } - - /** - * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart - * @returns {string} the status of the payment - */ - async getStatus(paymentData) { - const { resultCode } = paymentData - let status = "initial" - - if (resultCode === "Authorised") { - status = "authorized" - } - - return status - } - - async createPayment(_) { - return {} - } - - async authorizePayment(cart, paymentMethod) { - return this.adyenService_.authorizePayment(cart, paymentMethod) - } - - async retrievePayment(data) { - return this.adyenService_.retrievePayment(data) - } - - async updatePayment(data, _) { - return this.adyenService_.updatePayment(data) - } - - async deletePayment(data) { - return this.adyenService_.deletePayment(data) - } - - async capturePayment(data) { - try { - return this.adyenService_.capturePayment(data) - } catch (error) { - throw error - } - } - - async refundPayment(data) { - try { - return this.adyenService_.refundPayment(data) - } catch (error) { - throw error - } - } - - async cancelPayment(data) { - try { - return this.adyenService_.cancelPayment(data) - } catch (error) { - throw error - } - } -} - -export default CardAdyenService diff --git a/packages/medusa-payment-adyen/src/services/googlepay-adyen.js b/packages/medusa-payment-adyen/src/services/googlepay-adyen.js new file mode 100644 index 0000000000..2a24847d88 --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/googlepay-adyen.js @@ -0,0 +1,58 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class GooglePayAdyenService extends PaymentService { + static identifier = "paywithgoogle-adyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + async getStatus(paymentData) { + return this.adyenService_.getStatus(paymentData) + } + + async createPayment(data) { + return this.adyenService_.createPayment(data) + } + + async authorizePayment(sessionData, context) { + return this.adyenService_.authorizePayment(sessionData, context) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async updatePaymentData(sessionData, update) { + return this.adyenService_.updatePaymentData(sessionData, update) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + return this.adyenService_.capturePayment(data) + } + + async refundPayment(data, amountToRefund) { + return this.adyenService_.refundPayment(data, amountToRefund) + } + + async cancelPayment(data) { + return this.adyenService_.cancelPayment(data) + } +} + +export default GooglePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/googlepay.js b/packages/medusa-payment-adyen/src/services/googlepay.js deleted file mode 100644 index 30cbc08b88..0000000000 --- a/packages/medusa-payment-adyen/src/services/googlepay.js +++ /dev/null @@ -1,74 +0,0 @@ -import _ from "lodash" -import { PaymentService } from "medusa-interfaces" - -class GooglePayAdyenService extends PaymentService { - static identifier = "googlepayAdyen" - - constructor({ adyenService }) { - super() - - this.adyenService_ = adyenService - } - - /** - * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart - * @returns {string} the status of the payment - */ - async getStatus(paymentData) { - const { resultCode } = paymentData - let status = "initial" - - if (resultCode === "Authorised") { - status = "authorized" - } - - return status - } - - async createPayment(_) { - return {} - } - - async authorizePayment(cart, paymentMethod) { - return this.adyenService_.authorizePayment(cart, paymentMethod) - } - - async retrievePayment(data) { - return this.adyenService_.retrievePayment(data) - } - - async updatePayment(data, _) { - return this.adyenService_.updatePayment(data) - } - - async deletePayment(data) { - return this.adyenService_.deletePayment(data) - } - - async capturePayment(data) { - try { - return this.adyenService_.capturePayment(data) - } catch (error) { - throw error - } - } - - async refundPayment(data) { - try { - return this.adyenService_.refundPayment(data) - } catch (error) { - throw error - } - } - - async cancelPayment(data) { - try { - return this.adyenService_.cancelPayment(data) - } catch (error) { - throw error - } - } -} - -export default GooglePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/ideal-adyen.js b/packages/medusa-payment-adyen/src/services/ideal-adyen.js new file mode 100644 index 0000000000..c51c714d0b --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/ideal-adyen.js @@ -0,0 +1,62 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class IdealAdyenService extends PaymentService { + static identifier = "ideal-adyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + async getStatus(paymentData) { + return this.adyenService_.getStatus(paymentData) + } + + async createPayment(data) { + return this.adyenService_.createPayment(data) + } + + async authorizePayment(sessionData, context) { + return this.adyenService_.authorizePayment(sessionData, context) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async updatePaymentData(sessionData, update) { + return this.adyenService_.updatePaymentData(sessionData, update) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + return this.adyenService_.capturePayment(data) + } + + async refundPayment(data, amountToRefund) { + return this.adyenService_.refundPayment(data, amountToRefund) + } + + async cancelPayment(data) { + return this.adyenService_.cancelPayment(data) + } +} + +export default IdealAdyenService diff --git a/packages/medusa-payment-adyen/src/services/ideal.js b/packages/medusa-payment-adyen/src/services/ideal.js deleted file mode 100644 index b2904cd80b..0000000000 --- a/packages/medusa-payment-adyen/src/services/ideal.js +++ /dev/null @@ -1,74 +0,0 @@ -import _ from "lodash" -import { PaymentService } from "medusa-interfaces" - -class IdealAdyenService extends PaymentService { - static identifier = "idealAdyen" - - constructor({ adyenService }) { - super() - - this.adyenService_ = adyenService - } - - /** - * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart - * @returns {string} the status of the payment - */ - async getStatus(paymentData) { - const { resultCode } = paymentData - let status = "initial" - - if (resultCode === "Authorised") { - status = "authorized" - } - - return status - } - - async createPayment(_) { - return {} - } - - async authorizePayment(cart, paymentMethod) { - return this.adyenService_.authorizePayment(cart, paymentMethod) - } - - async retrievePayment(data) { - return this.adyenService_.retrievePayment(data) - } - - async updatePayment(data, _) { - return this.adyenService_.updatePayment(data) - } - - async deletePayment(data) { - return this.adyenService_.deletePayment(data) - } - - async capturePayment(data) { - try { - return this.adyenService_.capturePayment(data) - } catch (error) { - throw error - } - } - - async refundPayment(data) { - try { - return this.adyenService_.refundPayment(data) - } catch (error) { - throw error - } - } - - async cancelPayment(data) { - try { - return this.adyenService_.cancelPayment(data) - } catch (error) { - throw error - } - } -} - -export default IdealAdyenService diff --git a/packages/medusa-payment-adyen/src/services/mobilepay-adyen.js b/packages/medusa-payment-adyen/src/services/mobilepay-adyen.js new file mode 100644 index 0000000000..9df408e14d --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/mobilepay-adyen.js @@ -0,0 +1,58 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class MobilePayAdyenService extends PaymentService { + static identifier = "mobilepay-adyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + async getStatus(paymentData) { + return this.adyenService_.getStatus(paymentData) + } + + async createPayment(data) { + return this.adyenService_.createPayment(data) + } + + async authorizePayment(sessionData, context) { + return this.adyenService_.authorizePayment(sessionData, context) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async updatePaymentData(sessionData, update) { + return this.adyenService_.updatePaymentData(sessionData, update) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + return this.adyenService_.capturePayment(data) + } + + async refundPayment(data, amountToRefund) { + return this.adyenService_.refundPayment(data, amountToRefund) + } + + async cancelPayment(data) { + return this.adyenService_.cancelPayment(data) + } +} + +export default MobilePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/mobilepay.js b/packages/medusa-payment-adyen/src/services/mobilepay.js deleted file mode 100644 index fc29341cf6..0000000000 --- a/packages/medusa-payment-adyen/src/services/mobilepay.js +++ /dev/null @@ -1,74 +0,0 @@ -import _ from "lodash" -import { PaymentService } from "medusa-interfaces" - -class MobilePayAdyenService extends PaymentService { - static identifier = "mobilepayAdyen" - - constructor({ adyenService }) { - super() - - this.adyenService_ = adyenService - } - - /** - * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart - * @returns {string} the status of the payment - */ - async getStatus(paymentData) { - const { resultCode } = paymentData - // let status = "initial" - - // if (resultCode === "Authorised") { - // status = "authorized" - // } - - return "authorized" - } - - async createPayment(_) { - return {} - } - - async authorizePayment(cart, paymentMethod) { - return this.adyenService_.authorizePayment(cart, paymentMethod) - } - - async retrievePayment(data) { - return this.adyenService_.retrievePayment(data) - } - - async updatePayment(data, _) { - return this.adyenService_.updatePayment(data) - } - - async deletePayment(data) { - return this.adyenService_.deletePayment(data) - } - - async capturePayment(data) { - try { - return this.adyenService_.capturePayment(data) - } catch (error) { - throw error - } - } - - async refundPayment(data) { - try { - return this.adyenService_.refundPayment(data) - } catch (error) { - throw error - } - } - - async cancelPayment(data) { - try { - return this.adyenService_.cancelPayment(data) - } catch (error) { - throw error - } - } -} - -export default MobilePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/paypal-adyen.js b/packages/medusa-payment-adyen/src/services/paypal-adyen.js new file mode 100644 index 0000000000..e7aefa023f --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/paypal-adyen.js @@ -0,0 +1,58 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class PayPalAdyenService extends PaymentService { + static identifier = "paypal-adyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + async getStatus(paymentData) { + return this.adyenService_.getStatus(paymentData) + } + + async createPayment(data) { + return this.adyenService_.createPayment(data) + } + + async authorizePayment(sessionData, context) { + return this.adyenService_.authorizePayment(sessionData, context) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async getPaymentData(data) { + return this.adyenService_.getPaymentData(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async updatePaymentData(sessionData, update) { + return this.adyenService_.updatePaymentData(sessionData, update) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + return this.adyenService_.capturePayment(data) + } + + async refundPayment(data, amountToRefund) { + return this.adyenService_.refundPayment(data, amountToRefund) + } + + async cancelPayment(data) { + return this.adyenService_.cancelPayment(data) + } +} + +export default PayPalAdyenService diff --git a/packages/medusa-payment-adyen/src/services/paypal.js b/packages/medusa-payment-adyen/src/services/paypal.js deleted file mode 100644 index 2cefb9861c..0000000000 --- a/packages/medusa-payment-adyen/src/services/paypal.js +++ /dev/null @@ -1,74 +0,0 @@ -import _ from "lodash" -import { PaymentService } from "medusa-interfaces" - -class PayPalAdyenService extends PaymentService { - static identifier = "paypalAdyen" - - constructor({ adyenService }) { - super() - - this.adyenService_ = adyenService - } - - /** - * Status for Adyen payment. - * @param {Object} paymentData - payment method data from cart - * @returns {string} the status of the payment - */ - async getStatus(paymentData) { - const { resultCode } = paymentData - let status = "initial" - - if (resultCode === "Authorised") { - status = "authorized" - } - - return status - } - - async createPayment(_) { - return {} - } - - async authorizePayment(cart, paymentMethod) { - return this.adyenService_.authorizePayment(cart, paymentMethod) - } - - async retrievePayment(data) { - return this.adyenService_.retrievePayment(data) - } - - async updatePayment(data, _) { - return this.adyenService_.updatePayment(data) - } - - async deletePayment(data) { - return this.adyenService_.deletePayment(data) - } - - async capturePayment(data) { - try { - return this.adyenService_.capturePayment(data) - } catch (error) { - throw error - } - } - - async refundPayment(data) { - try { - return this.adyenService_.refundPayment(data) - } catch (error) { - throw error - } - } - - async cancelPayment(data) { - try { - return this.adyenService_.cancelPayment(data) - } catch (error) { - throw error - } - } -} - -export default PayPalAdyenService diff --git a/packages/medusa-payment-adyen/src/subscribers/adyen.js b/packages/medusa-payment-adyen/src/subscribers/adyen.js new file mode 100644 index 0000000000..2c140d518e --- /dev/null +++ b/packages/medusa-payment-adyen/src/subscribers/adyen.js @@ -0,0 +1,159 @@ +class AdyenSubscriber { + constructor(container) { + /** @private @const {CartService} */ + this.cartService_ = container.cartService + + /** @private @const {OrderService} */ + this.orderService_ = container.orderService + + /** @private @const {EventBusService} */ + this.eventBus_ = container.eventBusService + + /** @private @const {PaymentRepository} */ + this.paymentRepository_ = container.paymentRepository + + this.manager_ = container.manager + + this.eventBus_.subscribe( + "adyen.notification_received", + async (notification) => this.handleAdyenNotification(notification) + ) + } + + /** + * Webhook handler for Adyen payment. + * @param {object} notification - webhook notification object + * @returns {string} the status of the payment intent + */ + async handleAdyenNotification(notification) { + switch (true) { + case notification.success === "true" && + notification.eventCode === "AUTHORISATION": { + this.handleAuthorization_(notification) + break + } + case notification.success === "false" && + notification.eventCode === "AUTHORISATION": { + this.handleFailedAuthorization_(notification) + break + } + case notification.success === "true" && + notification.eventCode === "CAPTURE_FAILED": { + this.handleFailedCapture_(notification) + break + } + case notification.success === "false" && + notification.eventCode === "CAPTURE": { + this.handleFailedCapture_(notification) + break + } + case notification.success === "false" && + notification.eventCode === "REFUND": { + this.handleFailedRefund_(notification) + break + } + default: + break + } + } + + async handleFailedAuthorization_(notification) { + const cartId = notification.additionalData["metadata.cart_id"] + const cart = await this.cartService_.retrieve(cartId, { + relations: ["payment_sessions"], + }) + + const { payment_session } = cart + + const updated = { + ...payment_session, + status: "error", + data: { + ...payment_session.data, + pspReference: notification.pspReference, + }, + } + + await this.cartService_.updatePaymentSession(cart.id, updated) + } + + async handleAuthorization_(notification) { + const cartId = notification.additionalData["metadata.cart_id"] + const paymentRepository = this.manager_.getCustomRepository( + this.paymentRepository_ + ) + + // We need to ensure, that an order is created in situations, where the + // customer might have closed their browser prior to order creation + try { + const orderPayment = await paymentRepository.findOne({ + where: { cart_id: cartId }, + }) + + if (!orderPayment) { + throw new Error("Payment not found") + } + + const updatedPayment = { + ...orderPayment, + data: { + ...orderPayment.data, + resultCode: "Authorised", + pspReference: notification.pspReference, + }, + } + + await this.paymentRepository_.save(updatedPayment) + } catch (error) { + console.log(error) + await this.manager_.transaction(async (manager) => { + const session = { + pspReference: notification.pspReference, + resultCode: "Authorised", + } + + await this.cartService_ + .withTransaction(manager) + .updatePaymentSession(cartId, session) + + await this.cartService_ + .withTransaction(manager) + .authorizePayment(cartId) + + await this.orderService_.withTransaction(manager).createFromCart(cartId) + }) + } + } + + async handleFailedCapture_(notification) { + const cartId = notification.additionalData["metadata.cart_id"] + + const order = await this.orderService_.retrieveByCartId(cartId) + + await this.orderService_.update(order.id, { + payment_status: "requires_action", + }) + + await this.eventBus_.emit("order.payment_capture_failed", { + order, + error: new Error(`Adyen payment capture: ${notification.reason}`), + }) + } + + async handleFailedRefund_(notification) { + const cartId = notification.additionalData["metadata.cart_id"] + + const order = await this.orderService_.retrieveByCartId(cartId) + + await this.orderService_.update(order.id, { + payment_status: "requires_action", + }) + + await this.eventBus_.emit("order.refund_failed", { + order, + error: new Error(`Adyen payment capture: ${notification.reason}`), + }) + } +} + +export default AdyenSubscriber diff --git a/packages/medusa-payment-klarna/package.json b/packages/medusa-payment-klarna/package.json index 6b4414bdae..f13aed65de 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.0.14", + "version": "1.0.13-alpha.176+0646bd3", "description": "Klarna Payment provider for Medusa Commerce", "main": "index.js", "repository": { @@ -40,8 +40,8 @@ "axios": "^0.21.0", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-payment-klarna/src/__mocks__/cart.js b/packages/medusa-payment-klarna/src/__mocks__/cart.js index 851b6b7cf7..43d21984a3 100644 --- a/packages/medusa-payment-klarna/src/__mocks__/cart.js +++ b/packages/medusa-payment-klarna/src/__mocks__/cart.js @@ -2,13 +2,17 @@ import { IdMap } from "medusa-test-utils" export const carts = { frCart: { - _id: IdMap.getId("fr-cart"), + id: IdMap.getId("fr-cart"), email: "lebron@james.com", title: "test", + region: { + tax_rate: 2500, + currency_code: "eur", + }, region_id: IdMap.getId("region-france"), items: [ { - _id: IdMap.getId("line"), + id: IdMap.getId("line"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", @@ -16,20 +20,20 @@ export const carts = { { unit_price: 8, variant: { - _id: IdMap.getId("eur-8-us-10"), + id: IdMap.getId("eur-8-us-10"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, { unit_price: 10, variant: { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -37,17 +41,17 @@ export const carts = { quantity: 10, }, { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 10, variant: { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -56,13 +60,16 @@ export const carts = { ], shipping_methods: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), + data: { + name: "test", + }, profile_id: "default_profile", }, ], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], @@ -87,7 +94,7 @@ export const carts = { discounts: [ { code: "MEDUSA_FREE", - discount_rule: { + rule: { type: "percent", value: 20, allocation: "item", diff --git a/packages/medusa-payment-klarna/src/api/routes/hooks/address.js b/packages/medusa-payment-klarna/src/api/routes/hooks/address.js index 302d1bc795..f00cd85c75 100644 --- a/packages/medusa-payment-klarna/src/api/routes/hooks/address.js +++ b/packages/medusa-payment-klarna/src/api/routes/hooks/address.js @@ -3,38 +3,107 @@ export default async (req, res) => { const { shipping_address, merchant_data } = req.body try { + const manager = req.scope.resolve("manager") const cartService = req.scope.resolve("cartService") const klarnaProviderService = req.scope.resolve("pp_klarna") const shippingProfileService = req.scope.resolve("shippingProfileService") - const cart = await cartService.retrieve(merchant_data) + const result = await manager.transaction("SERIALIZABLE", async (m) => { + const cart = await cartService.retrieve(merchant_data, { + select: ["subtotal"], + relations: [ + "shipping_address", + "billing_address", + "region", + "items", + "shipping_methods", + "shipping_methods.shipping_option", + "items.variant", + "items.variant.product", + ], + }) - if (shipping_address) { - const updatedAddress = { - first_name: shipping_address.given_name, - last_name: shipping_address.family_name, - address_1: shipping_address.street_address, - address_2: shipping_address.street_address2, - city: shipping_address.city, - country_code: shipping_address.country.toUpperCase(), - postal_code: shipping_address.postal_code, - phone: shipping_address.phone + if (shipping_address) { + const shippingAddress = { + ...cart.shipping_address, + first_name: shipping_address.given_name, + last_name: shipping_address.family_name, + address_1: shipping_address.street_address, + address_2: shipping_address.street_address2, + city: shipping_address.city, + country_code: shipping_address.country, + postal_code: shipping_address.postal_code, + phone: shipping_address.phone, + } + + let billingAddress = { + first_name: shipping_address.given_name, + last_name: shipping_address.family_name, + address_1: shipping_address.street_address, + address_2: shipping_address.street_address2, + city: shipping_address.city, + country_code: shipping_address.country, + postal_code: shipping_address.postal_code, + phone: shipping_address.phone, + } + + if (cart.billing_address) { + billingAddress = { + ...billingAddress, + ...cart.billing_address, + } + } + + await cartService.update(cart.id, { + shipping_address: shippingAddress, + billing_address: billingAddress, + email: shipping_address.email, + }) + + const shippingOptions = await shippingProfileService.fetchCartOptions( + cart + ) + + if (shippingOptions?.length) { + const option = shippingOptions.find( + (o) => o.data && !o.data.require_drop_point + ) + await cartService + .withTransaction(m) + .addShippingMethod(cart.id, option.id, option.data) + } + + // Fetch and return updated Klarna order + const updatedCart = await cartService + .withTransaction(m) + .retrieve(cart.id, { + select: [ + "gift_card_total", + "subtotal", + "total", + "tax_total", + "discount_total", + "subtotal", + ], + relations: [ + "shipping_address", + "billing_address", + "region", + "shipping_methods", + "shipping_methods.shipping_option", + "items", + "items.variant", + "items.variant.product", + ], + }) + return klarnaProviderService.cartToKlarnaOrder(updatedCart) + } else { + return null } - - await cartService.updateShippingAddress(cart._id, updatedAddress) - await cartService.updateBillingAddress(cart._id, updatedAddress) - await cartService.updateEmail(cart._id, shipping_address.email) + }) - const shippingOptions = await shippingProfileService.fetchCartOptions(cart) - if (shippingOptions.length === 1) { - const option = shippingOptions[0] - await cartService.addShippingMethod(cart._id, option._id, option.data) - } - - // Fetch and return updated Klarna order - const updatedCart = await cartService.retrieve(cart._id) - const order = await klarnaProviderService.cartToKlarnaOrder(updatedCart) - res.json(order) + if (result) { + res.json(result) return } else { res.sendStatus(400) diff --git a/packages/medusa-payment-klarna/src/api/routes/hooks/push.js b/packages/medusa-payment-klarna/src/api/routes/hooks/push.js index 90e378602a..f6fb142951 100644 --- a/packages/medusa-payment-klarna/src/api/routes/hooks/push.js +++ b/packages/medusa-payment-klarna/src/api/routes/hooks/push.js @@ -4,7 +4,6 @@ export default async (req, res) => { const { klarna_order_id } = req.query try { - const cartService = req.scope.resolve("cartService") const orderService = req.scope.resolve("orderService") const klarnaProviderService = req.scope.resolve("pp_klarna") @@ -13,22 +12,9 @@ export default async (req, res) => { ) const cartId = klarnaOrder.merchant_data - try { - const order = await orderService.retrieveByCartId(cartId) - await klarnaProviderService.acknowledgeOrder( - klarnaOrder.order_id, - order._id - ) - } catch (err) { - if (err.type === MedusaError.Types.NOT_FOUND) { - const cart = await cartService.retrieve(cartId) - const order = await orderService.createFromCart(cart) - await klarnaProviderService.acknowledgeOrder( - klarnaOrder.order_id, - order._id - ) - } - } + const order = await orderService.retrieveByCartId(cartId) + + await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order.id) res.sendStatus(200) } catch (error) { diff --git a/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js b/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js index 633c8390d2..cf51406d25 100644 --- a/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js +++ b/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js @@ -7,19 +7,57 @@ export default async (req, res) => { const klarnaProviderService = req.scope.resolve("pp_klarna") const shippingProfileService = req.scope.resolve("shippingProfileService") - const cart = await cartService.retrieve(merchant_data) - const shippingOptions = await shippingProfileService.fetchCartOptions(cart) + const cart = await cartService.retrieve(merchant_data, { + select: ["subtotal"], + relations: [ + "shipping_address", + "billing_address", + "region", + "shipping_methods", + "shipping_methods.shipping_option", + "items", + "items.variant", + "items.variant.product", + ], + }) + let shippingOptions = await shippingProfileService.fetchCartOptions(cart) + + shippingOptions = shippingOptions.filter( + (so) => !so.data?.require_drop_point + ) const ids = selected_shipping_option.id.split(".") - await Promise.all(ids.map(async id => { - const option = shippingOptions.find(({ _id }) => _id.equals(id)) + await Promise.all( + ids.map(async (id) => { + const option = shippingOptions.find((so) => so.id === id) - if (option) { - await cartService.addShippingMethod(cart._id, option._id, option.data) - } - })) + if (option) { + await cartService.addShippingMethod(cart.id, option.id, option.data) + } + }) + ) + + const newCart = await cartService.retrieve(cart.id, { + select: [ + "gift_card_total", + "subtotal", + "total", + "tax_total", + "discount_total", + "subtotal", + ], + relations: [ + "shipping_address", + "billing_address", + "shipping_methods", + "shipping_methods.shipping_option", + "region", + "items", + "items.variant", + "items.variant.product", + ], + }) - const newCart = await cartService.retrieve(cart._id) const order = await klarnaProviderService.cartToKlarnaOrder(newCart) res.json(order) } catch (error) { diff --git a/packages/medusa-payment-klarna/src/services/__tests__/klarna-provider.js b/packages/medusa-payment-klarna/src/services/__tests__/klarna-provider.js index 5f58277645..0d0f1b469c 100644 --- a/packages/medusa-payment-klarna/src/services/__tests__/klarna-provider.js +++ b/packages/medusa-payment-klarna/src/services/__tests__/klarna-provider.js @@ -1,3 +1,5 @@ +jest.unmock("axios") +import axios from "axios" import MockAdapter from "axios-mock-adapter" import KlarnaProviderService from "../klarna-provider" @@ -151,7 +153,6 @@ describe("KlarnaProviderService", () => { expect(result).toEqual({ order_id: "123456789", - order_amount: 1000, }) }) }) @@ -176,15 +177,19 @@ describe("KlarnaProviderService", () => { mockServer .onPost("/ordermanagement/v1/orders/123456789/cancel") .reply(() => { - return [200] + return [200, { order_id: "123456789" }] }) - it("returns order id", async () => { + mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => { + return [200, { order_id: "123456789" }] + }) + + it("returns order", async () => { result = await klarnaProviderService.cancelPayment({ - order_id: "123456789", + data: { order_id: "123456789" }, }) - expect(result).toEqual("123456789") + expect(result).toEqual({ order_id: "123456789" }) }) }) @@ -289,15 +294,19 @@ describe("KlarnaProviderService", () => { mockServer .onPost("/ordermanagement/v1/orders/123456789/captures") .reply(() => { - return [200] + return [200, { order_id: "123456789" }] }) - it("returns order id", async () => { + mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => { + return [200, { order_id: "123456789" }] + }) + + it("returns order", async () => { result = await klarnaProviderService.capturePayment({ - order_id: "123456789", + data: { order_id: "123456789" }, }) - expect(result).toEqual("123456789") + expect(result).toEqual({ order_id: "123456789" }) }) }) @@ -322,18 +331,22 @@ describe("KlarnaProviderService", () => { mockServer .onPost("/ordermanagement/v1/orders/123456789/refunds") .reply(() => { - return [200] + return [200, { order_id: "123456789" }] }) - it("returns order id", async () => { + mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => { + return [200, { order_id: "123456789" }] + }) + + it("returns order", async () => { result = await klarnaProviderService.refundPayment( { - order_id: "123456789", + data: { order_id: "123456789" }, }, 1000 ) - expect(result).toEqual("123456789") + expect(result).toEqual({ order_id: "123456789" }) }) }) }) diff --git a/packages/medusa-payment-klarna/src/services/klarna-provider.js b/packages/medusa-payment-klarna/src/services/klarna-provider.js index 71160356eb..6bcaacdd9d 100644 --- a/packages/medusa-payment-klarna/src/services/klarna-provider.js +++ b/packages/medusa-payment-klarna/src/services/klarna-provider.js @@ -5,14 +5,26 @@ import { PaymentService } from "medusa-interfaces" class KlarnaProviderService extends PaymentService { static identifier = "klarna" - constructor( - { shippingProfileService, totalsService, regionService }, - options - ) { + constructor({ shippingProfileService }, options) { super() + /** + * Required Klarna options: + * { + * backend_url: "", + * url: "", + * user: "", + * password: "", + * merchant_urls: { + * terms: ``, + * checkout: ``, + * confirmation: ``, + * } + * } + */ this.options_ = options + /** @private @const {Klarna} */ this.klarna_ = axios.create({ baseURL: options.url, auth: { @@ -27,54 +39,36 @@ class KlarnaProviderService extends PaymentService { this.backendUrl_ = options.backend_url - this.totalsService_ = totalsService - - this.regionService_ = regionService - + /** @private @const {ShippingProfileService} */ this.shippingProfileService_ = shippingProfileService } async lineItemsToOrderLines_(cart, taxRate) { let order_lines = [] + const tax = taxRate / 100 + cart.items.forEach((item) => { - // For bundles, we create an order line for each item in the bundle - if (Array.isArray(item.content)) { - item.content.forEach((c) => { - const total_amount = c.unit_price * c.quantity * (taxRate + 1) - const total_tax_amount = total_amount * taxRate + // Withdraw discount from the total item amount + const quantity = item.quantity + const unit_price = item.unit_price * (tax + 1) + const total_amount = unit_price * quantity + const total_tax_amount = total_amount * (tax / (1 + tax)) - order_lines.push({ - name: item.title, - unit_price: c.unit_price, - quantity: c.quantity, - tax_rate: taxRate * 10000, - total_amount, - total_tax_amount, - }) - }) - } else { - // Withdraw discount from the total item amount - const quantity = item.quantity - const unit_price = item.content.unit_price * 100 * (taxRate + 1) - const total_amount = unit_price * quantity - const total_tax_amount = total_amount * (taxRate / (1 + taxRate)) - - order_lines.push({ - name: item.title, - tax_rate: taxRate * 10000, - quantity, - unit_price, - total_amount, - total_tax_amount, - }) - } + order_lines.push({ + name: item.title, + tax_rate: tax * 10000, + quantity, + unit_price, + total_amount, + total_tax_amount, + }) }) if (cart.shipping_methods.length) { const { name, price } = cart.shipping_methods.reduce( (acc, next) => { - acc.name = [...acc.name, next.name] + acc.name = [...acc.name, next.data.name] acc.price += next.price return acc }, @@ -85,10 +79,10 @@ class KlarnaProviderService extends PaymentService { name: name.join(" + "), quantity: 1, type: "shipping_fee", - unit_price: price * (1 + taxRate) * 100, - tax_rate: taxRate * 10000, - total_amount: price * (1 + taxRate) * 100, - total_tax_amount: price * taxRate * 100, + unit_price: price * (1 + tax), + tax_rate: tax * 10000, + total_amount: price * (1 + tax), + total_tax_amount: price * tax, }) } @@ -98,28 +92,39 @@ class KlarnaProviderService extends PaymentService { async cartToKlarnaOrder(cart) { let order = { // Cart id is stored, such that we can use it for hooks - merchant_data: cart._id, - // TODO: Investigate if other locales are needed + merchant_data: cart.id, locale: "en-US", } - const { tax_rate, currency_code } = await this.regionService_.retrieve( - cart.region_id - ) + const { region, gift_card_total, discount_total, tax_total, total } = cart - order.order_lines = await this.lineItemsToOrderLines_(cart, tax_rate) + const taxRate = region.tax_rate / 100 - const discount = (await this.totalsService_.getDiscountTotal(cart)) * 100 - if (discount) { + order.order_lines = await this.lineItemsToOrderLines_(cart, region.tax_rate) + + if (discount_total) { order.order_lines.push({ name: `Discount`, quantity: 1, type: "discount", unit_price: 0, - total_discount_amount: discount * (1 + tax_rate), - tax_rate: tax_rate * 10000, - total_amount: -discount * (1 + tax_rate), - total_tax_amount: -discount * tax_rate, + total_discount_amount: discount_total * (1 + taxRate), + tax_rate: taxRate * 10000, + total_amount: -discount_total * (1 + taxRate), + total_tax_amount: -discount_total * taxRate, + }) + } + + if (gift_card_total) { + order.order_lines.push({ + name: `Gift Card`, + quantity: 1, + type: "gift_card", + unit_price: 0, + total_discount_amount: gift_card_total * (1 + taxRate), + tax_rate: taxRate * 10000, + total_amount: -gift_card_total * (1 + taxRate), + total_tax_amount: -gift_card_total * taxRate, }) } @@ -134,20 +139,19 @@ class KlarnaProviderService extends PaymentService { } } - // TODO: Check if country matches ISO - if ( - !_.isEmpty(cart.shipping_address) && - cart.shipping_address.country_code - ) { - order.purchase_country = cart.shipping_address.country_code + const hasCountry = + !_.isEmpty(cart.shipping_address) && cart.shipping_address.country_code + + if (hasCountry) { + order.purchase_country = cart.shipping_address.country_code.toUpperCase() } else { // Defaults to Sweden order.purchase_country = "SE" } - order.order_amount = (await this.totalsService_.getTotal(cart)) * 100 - order.order_tax_amount = (await this.totalsService_.getTaxTotal(cart)) * 100 - // TODO: Check if currency matches ISO - order.purchase_currency = currency_code + + order.order_amount = total + order.order_tax_amount = tax_total + order.purchase_currency = region.currency_code.toUpperCase() order.merchant_urls = { terms: this.options_.merchant_urls.terms, @@ -159,20 +163,24 @@ class KlarnaProviderService extends PaymentService { } if (cart.shipping_address && cart.shipping_address.first_name) { - const shippingOptions = await this.shippingProfileService_.fetchCartOptions( + let shippingOptions = await this.shippingProfileService_.fetchCartOptions( cart ) + shippingOptions = shippingOptions.filter( + (so) => !so.data?.require_drop_point + ) + // If the cart does not have shipping methods yet, preselect one from // shipping_options and set the selected shipping method if (cart.shipping_methods.length) { const shipping_method = cart.shipping_methods[0] order.selected_shipping_option = { - id: shipping_method._id, - name: shipping_method.name, - price: shipping_method.price * (1 + tax_rate) * 100, - tax_amount: shipping_method.price * tax_rate * 100, - tax_rate: tax_rate * 10000, + id: shipping_method.shipping_option.id, + name: shipping_method.shipping_option.name, + price: shipping_method.price * (1 + taxRate), + tax_amount: shipping_method.price * taxRate, + tax_rate: taxRate * 10000, } } @@ -185,20 +193,25 @@ class KlarnaProviderService extends PaymentService { return acc }, {}) - let f = (a, b) => - [].concat(...a.map((a) => b.map((b) => [].concat(a, b)))) - let cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a) + // Helper function that calculates the cartesian product of multiple arrays + // Don't touch :D + // From: https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript + const f = (a, b) => + [].concat(...a.map((d) => b.map((e) => [].concat(d, e)))) + const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a) const methods = Object.keys(partitioned).map((k) => partitioned[k]) const combinations = cartesian(...methods) + // Use the cartesian product of shipping methods to generate correct + // format for the Klarna Widget order.shipping_options = combinations.map((combination) => { combination = Array.isArray(combination) ? combination : [combination] const details = combination.reduce( (acc, next) => { - acc.id = [...acc.id, next._id] + acc.id = [...acc.id, next.id] acc.name = [...acc.name, next.name] - acc.price += next.price + acc.price += next.amount return acc }, { id: [], name: [], price: 0 } @@ -207,10 +220,9 @@ class KlarnaProviderService extends PaymentService { return { id: details.id.join("."), name: details.name.join(" + "), - price: details.price * (1 + tax_rate) * 100, - tax_amount: details.price * tax_rate * 100, - tax_rate: tax_rate * 10000, - preselected: combinations.length === 1, + price: details.price * (1 + taxRate), + tax_amount: details.price * taxRate, + tax_rate: taxRate * 10000, } }) } @@ -224,20 +236,18 @@ class KlarnaProviderService extends PaymentService { * @returns {string} the status of the Klarna order */ async getStatus(paymentData) { - try { - const { order_id } = paymentData - const { data: order } = await this.klarna_.get( - `${this.klarnaOrderUrl_}/${order_id}` - ) + const { order_id } = paymentData + const { data: order } = await this.klarna_.get( + `${this.klarnaOrderUrl_}/${order_id}` + ) - let status = "initial" - if (order.status === "checkout_complete") { - status = "authorized" - } - return status - } catch (error) { - throw error + let status = "pending" + + if (order.status === "checkout_complete") { + status = "authorized" } + + return status } /** @@ -249,9 +259,12 @@ class KlarnaProviderService extends PaymentService { async createPayment(cart) { try { const order = await this.cartToKlarnaOrder(cart) - return this.klarna_ + + const klarnaPayment = await this.klarna_ .post(this.klarnaOrderUrl_, order) .then(({ data }) => data) + + return klarnaPayment } catch (error) { throw error } @@ -272,6 +285,21 @@ class KlarnaProviderService extends PaymentService { } } + /** + * Gets a Klarna payment objec. + * @param {object} sessionData - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent + */ + async getPaymentData(sessionData) { + try { + return this.klarna_ + .get(`${this.klarnaOrderUrl_}/${sessionData.data.order_id}`) + .then(({ data }) => data) + } catch (error) { + throw error + } + } + /** * Retrieves completed Klarna Order. * @param {string} klarnaOrderId - id of the order to retrieve @@ -287,6 +315,23 @@ class KlarnaProviderService extends PaymentService { } } + /** + * Authorizes Klarna payment by simply returning the status for the payment + * 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 = {}) { + try { + const paymentStatus = await this.getStatus(sessionData.data) + + return { data: sessionData.data, status: paymentStatus } + } catch (error) { + throw error + } + } + /** * Acknowledges a Klarna order as part of the order completion process * @param {string} klarnaOrderId - id of the order to acknowledge @@ -332,6 +377,14 @@ class KlarnaProviderService extends PaymentService { } } + async updatePaymentData(sessionData, update) { + try { + return { ...sessionData, ...update } + } catch (error) { + throw error + } + } + /** * Updates Klarna order. * @param {string} order - the order to update @@ -339,14 +392,14 @@ class KlarnaProviderService extends PaymentService { * @returns {Object} updated order */ async updatePayment(paymentData, cart) { - try { - const order = await this.cartToKlarnaOrder(cart, true) + if (cart.total !== paymentData.order_amount) { + const order = await this.cartToKlarnaOrder(cart) return this.klarna_ .post(`${this.klarnaOrderUrl_}/${paymentData.order_id}`, order) .then(({ data }) => data) - } catch (error) { - throw error } + + return paymentData } /** @@ -354,9 +407,9 @@ class KlarnaProviderService extends PaymentService { * @param {Object} paymentData - payment method data from cart * @returns {string} id of captured order */ - async capturePayment(paymentData) { + async capturePayment(payment) { + const { order_id } = payment.data try { - const { order_id } = paymentData const { data: order } = await this.klarna_.get( `${this.klarnaOrderManagementUrl_}/${order_id}` ) @@ -368,7 +421,8 @@ class KlarnaProviderService extends PaymentService { captured_amount: order_amount, } ) - return order_id + + return this.retrieveCompletedOrder(order_id) } catch (error) { throw error } @@ -379,16 +433,17 @@ class KlarnaProviderService extends PaymentService { * @param {Object} paymentData - payment method data from cart * @returns {string} id of refunded order */ - async refundPayment(paymentData, amount) { + async refundPayment(payment, amountToRefund) { + const { order_id } = payment.data try { - const { order_id } = paymentData await this.klarna_.post( `${this.klarnaOrderManagementUrl_}/${order_id}/refunds`, { - refunded_amount: amount * 100, + refunded_amount: amountToRefund, } ) - return order_id + + return this.retrieveCompletedOrder(order_id) } catch (error) { throw error } @@ -399,17 +454,22 @@ class KlarnaProviderService extends PaymentService { * @param {Object} paymentData - payment method data from cart * @returns {string} id of cancelled order */ - async cancelPayment(paymentData) { + async cancelPayment(payment) { + const { order_id } = payment.data try { - const { order_id } = paymentData await this.klarna_.post( `${this.klarnaOrderManagementUrl_}/${order_id}/cancel` ) - return order_id + + return this.retrieveCompletedOrder(order_id) } catch (error) { throw error } } + + async deletePayment(_) { + return Promise.resolve() + } } export default KlarnaProviderService diff --git a/packages/medusa-payment-klarna/yarn.lock b/packages/medusa-payment-klarna/yarn.lock index 01c50794f0..f4737aefd3 100644 --- a/packages/medusa-payment-klarna/yarn.lock +++ b/packages/medusa-payment-klarna/yarn.lock @@ -3595,7 +3595,7 @@ joi-objectid@^3.0.1: resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== -joi@^17.2.1: +joi@^17.2.1, joi@^17.3.0: version "17.3.0" resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2" integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg== @@ -3857,21 +3857,29 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.10.tgz#6175f1e428318205742621f78d387254fc85b8a1" - integrity sha512-z12ITzPl5UpDJTILNXcMGD4yujSRkuvVtxkrvqCmA44IEyRj1hWZ0dGbVzuHj2wIahDHFFg66oq91IQH5QVsyg== +medusa-core-utils@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.11.tgz#c77685c4b637f6aebe808e82dc4f7739c2f9ff79" + integrity sha512-Nnp7RCWsR4MJNrslC0YB/wMO1Wf2QJH0aras+tjRdymXfR/ySn2/0+v8pyXxhY8XXYzOZknnjdCB8KI9+PHlsA== dependencies: joi "^17.2.1" joi-objectid "^3.0.1" -medusa-test-utils@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.11.tgz#bae901efa90426fb64818de700dc6e163820160f" - integrity sha512-CSNb70sXOfKTndzxWtPMYq+KeYaFkSLAPUyhuwDNVvkbne0faOYF5OMCV8r3aLoQ3D1Tvjrb/XB9Qt17ycjuPw== +medusa-core-utils@^1.0.12-alpha.787+0646bd3: + version "1.0.12-alpha.801" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.12-alpha.801.tgz#0a7d0b5d4b2290d9a2f7afb35ab1f0bec666dbb0" + integrity sha512-1ppxgl8noZfO3JLLHeLj4eEMOwTI/LcCxWwwD7AUp5uP1brPmctthDTM/pOMrEuytKWIqaNXCRohvAlgDr0vYA== + dependencies: + joi "^17.3.0" + joi-objectid "^3.0.1" + +medusa-test-utils@^1.0.12-alpha.176+0646bd3: + version "1.0.13" + resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.13.tgz#f728b8dacb1ba64d85529e3f2e9dba6c7e315416" + integrity sha512-EBbjGLHFEG/Fnqneire/bNCPeseo1ZFF3n3yMUfp0JDmBAVgE8L52wJa8Zeu36TL/rbQsQAsTAS6La46A8IJgg== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "^1.0.10" + medusa-core-utils "^1.0.11" mongoose "^5.8.0" memory-pager@^1.0.2: diff --git a/packages/medusa-payment-stripe/.babelrc b/packages/medusa-payment-stripe/.babelrc index 4d2dfe8f09..75cbf1558b 100644 --- a/packages/medusa-payment-stripe/.babelrc +++ b/packages/medusa-payment-stripe/.babelrc @@ -1,5 +1,6 @@ { "plugins": [ + "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-instanceof", "@babel/plugin-transform-classes" diff --git a/packages/medusa-payment-stripe/.gitignore b/packages/medusa-payment-stripe/.gitignore index 2ca7f03256..880606e4d1 100644 --- a/packages/medusa-payment-stripe/.gitignore +++ b/packages/medusa-payment-stripe/.gitignore @@ -12,5 +12,4 @@ yarn.lock /services /models /subscribers -/__mocks__ - +/__mocks__ \ No newline at end of file diff --git a/packages/medusa-payment-stripe/.npmignore b/packages/medusa-payment-stripe/.npmignore index 486581be18..73122644c5 100644 --- a/packages/medusa-payment-stripe/.npmignore +++ b/packages/medusa-payment-stripe/.npmignore @@ -5,5 +5,9 @@ node_modules /*.js !index.js yarn.lock - +src +.gitignore +.eslintrc +.babelrc +.prettierrc diff --git a/packages/medusa-payment-stripe/__mocks__/cart.js b/packages/medusa-payment-stripe/__mocks__/cart.js index d628d6cd22..d1bba8c35a 100644 --- a/packages/medusa-payment-stripe/__mocks__/cart.js +++ b/packages/medusa-payment-stripe/__mocks__/cart.js @@ -7,13 +7,17 @@ exports["default"] = exports.CartServiceMock = exports.carts = void 0; var _medusaTestUtils = require("medusa-test-utils"); +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + var carts = { emptyCart: { - _id: _medusaTestUtils.IdMap.getId("emptyCart"), + id: _medusaTestUtils.IdMap.getId("emptyCart"), items: [], region_id: _medusaTestUtils.IdMap.getId("testRegion"), + customer_id: "test-customer", + payment_sessions: [], shipping_options: [{ - _id: _medusaTestUtils.IdMap.getId("freeShipping"), + id: _medusaTestUtils.IdMap.getId("freeShipping"), profile_id: "default_profile", data: { some_data: "yes" @@ -21,58 +25,53 @@ var carts = { }] }, frCart: { - _id: _medusaTestUtils.IdMap.getId("fr-cart"), + id: _medusaTestUtils.IdMap.getId("fr-cart"), email: "lebron@james.com", title: "test", region_id: _medusaTestUtils.IdMap.getId("region-france"), items: [{ - _id: _medusaTestUtils.IdMap.getId("line"), + id: _medusaTestUtils.IdMap.getId("line"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: [{ - unit_price: 8, - variant: { - _id: _medusaTestUtils.IdMap.getId("eur-8-us-10") - }, - product: { - _id: _medusaTestUtils.IdMap.getId("product") - }, - quantity: 1 - }, { - unit_price: 10, - variant: { - _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") - }, - product: { - _id: _medusaTestUtils.IdMap.getId("product") - }, - quantity: 1 - }], - quantity: 10 - }, { - _id: _medusaTestUtils.IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 10, - variant: { - _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") - }, - product: { - _id: _medusaTestUtils.IdMap.getId("product") - }, - quantity: 1 + unit_price: 8, + variant: { + id: _medusaTestUtils.IdMap.getId("eur-8-us-10") }, + product: { + id: _medusaTestUtils.IdMap.getId("product") + }, + // { + // unit_price: 10, + // variant: { + // id: IdMap.getId("eur-10-us-12"), + // }, + // product: { + // id: IdMap.getId("product"), + // }, + // quantity: 1, + // }, quantity: 10 - }], + }, _defineProperty({ + id: _medusaTestUtils.IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + unit_price: 10, + variant: { + id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + }, + product: { + id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }, "quantity", 10)], shipping_methods: [{ - _id: _medusaTestUtils.IdMap.getId("freeShipping"), + id: _medusaTestUtils.IdMap.getId("freeShipping"), profile_id: "default_profile" }], shipping_options: [{ - _id: _medusaTestUtils.IdMap.getId("freeShipping"), + id: _medusaTestUtils.IdMap.getId("freeShipping"), profile_id: "default_profile" }], payment_sessions: [{ @@ -95,57 +94,57 @@ var carts = { customer_id: _medusaTestUtils.IdMap.getId("lebron") }, frCartNoStripeCustomer: { - _id: _medusaTestUtils.IdMap.getId("fr-cart-no-customer"), + id: _medusaTestUtils.IdMap.getId("fr-cart-no-customer"), title: "test", region_id: _medusaTestUtils.IdMap.getId("region-france"), items: [{ - _id: _medusaTestUtils.IdMap.getId("line"), + id: _medusaTestUtils.IdMap.getId("line"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: [{ unit_price: 8, variant: { - _id: _medusaTestUtils.IdMap.getId("eur-8-us-10") + id: _medusaTestUtils.IdMap.getId("eur-8-us-10") }, product: { - _id: _medusaTestUtils.IdMap.getId("product") + id: _medusaTestUtils.IdMap.getId("product") }, quantity: 1 }, { unit_price: 10, variant: { - _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + id: _medusaTestUtils.IdMap.getId("eur-10-us-12") }, product: { - _id: _medusaTestUtils.IdMap.getId("product") + id: _medusaTestUtils.IdMap.getId("product") }, quantity: 1 }], quantity: 10 }, { - _id: _medusaTestUtils.IdMap.getId("existingLine"), + id: _medusaTestUtils.IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 10, variant: { - _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + id: _medusaTestUtils.IdMap.getId("eur-10-us-12") }, product: { - _id: _medusaTestUtils.IdMap.getId("product") + id: _medusaTestUtils.IdMap.getId("product") }, quantity: 1 }, quantity: 10 }], shipping_methods: [{ - _id: _medusaTestUtils.IdMap.getId("freeShipping"), + id: _medusaTestUtils.IdMap.getId("freeShipping"), profile_id: "default_profile" }], shipping_options: [{ - _id: _medusaTestUtils.IdMap.getId("freeShipping"), + id: _medusaTestUtils.IdMap.getId("freeShipping"), profile_id: "default_profile" }], payment_sessions: [{ @@ -175,6 +174,10 @@ var CartServiceMock = { return Promise.resolve(carts.frCart); } + if (cartId === _medusaTestUtils.IdMap.getId("fr-cart-no-customer")) { + return Promise.resolve(carts.frCartNoStripeCustomer); + } + if (cartId === _medusaTestUtils.IdMap.getId("emptyCart")) { return Promise.resolve(carts.emptyCart); } diff --git a/packages/medusa-payment-stripe/package.json b/packages/medusa-payment-stripe/package.json index c87d072df5..f4771640b1 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.0.15", + "version": "1.0.14-alpha.176+0646bd3", "description": "Stripe Payment provider for Meduas Commerce", "main": "index.js", "repository": { @@ -15,6 +15,7 @@ "@babel/core": "^7.7.5", "@babel/node": "^7.7.4", "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", "@babel/plugin-transform-classes": "^7.9.5", "@babel/plugin-transform-instanceof": "^7.8.3", "@babel/plugin-transform-runtime": "^7.7.6", @@ -25,7 +26,7 @@ "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.2", - "medusa-test-utils": "^1.0.13" + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, "scripts": { "build": "babel src -d . --ignore **/__tests__", @@ -39,8 +40,8 @@ "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", "stripe": "^8.50.0" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-payment-stripe/src/__mocks__/cart.js b/packages/medusa-payment-stripe/src/__mocks__/cart.js index ca3adb2fcd..361f80386d 100644 --- a/packages/medusa-payment-stripe/src/__mocks__/cart.js +++ b/packages/medusa-payment-stripe/src/__mocks__/cart.js @@ -2,12 +2,14 @@ import { IdMap } from "medusa-test-utils" export const carts = { emptyCart: { - _id: IdMap.getId("emptyCart"), + id: IdMap.getId("emptyCart"), items: [], region_id: IdMap.getId("testRegion"), + customer_id: "test-customer", + payment_sessions: [], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", data: { some_data: "yes", @@ -16,67 +18,61 @@ export const carts = { ], }, frCart: { - _id: IdMap.getId("fr-cart"), + id: IdMap.getId("fr-cart"), email: "lebron@james.com", title: "test", region_id: IdMap.getId("region-france"), items: [ { - _id: IdMap.getId("line"), + id: IdMap.getId("line"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: [ - { - unit_price: 8, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - ], + + unit_price: 8, + variant: { + id: IdMap.getId("eur-8-us-10"), + }, + product: { + id: IdMap.getId("product"), + }, + // { + // unit_price: 10, + // variant: { + // id: IdMap.getId("eur-10-us-12"), + // }, + // product: { + // id: IdMap.getId("product"), + // }, + // quantity: 1, + // }, quantity: 10, }, { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + unit_price: 10, + variant: { + id: IdMap.getId("eur-10-us-12"), }, + product: { + id: IdMap.getId("product"), + }, + quantity: 1, quantity: 10, }, ], shipping_methods: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], @@ -102,12 +98,12 @@ export const carts = { customer_id: IdMap.getId("lebron"), }, frCartNoStripeCustomer: { - _id: IdMap.getId("fr-cart-no-customer"), + id: IdMap.getId("fr-cart-no-customer"), title: "test", region_id: IdMap.getId("region-france"), items: [ { - _id: IdMap.getId("line"), + id: IdMap.getId("line"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", @@ -115,20 +111,20 @@ export const carts = { { unit_price: 8, variant: { - _id: IdMap.getId("eur-8-us-10"), + id: IdMap.getId("eur-8-us-10"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, { unit_price: 10, variant: { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -136,17 +132,17 @@ export const carts = { quantity: 10, }, { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 10, variant: { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -155,13 +151,13 @@ export const carts = { ], shipping_methods: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], @@ -193,6 +189,9 @@ export const CartServiceMock = { if (cartId === IdMap.getId("fr-cart")) { return Promise.resolve(carts.frCart) } + if (cartId === IdMap.getId("fr-cart-no-customer")) { + return Promise.resolve(carts.frCartNoStripeCustomer) + } if (cartId === IdMap.getId("emptyCart")) { return Promise.resolve(carts.emptyCart) } diff --git a/packages/medusa-payment-stripe/src/api/routes/hooks/stripe.js b/packages/medusa-payment-stripe/src/api/routes/hooks/stripe.js index 7c77cd3fac..2c1c1771b6 100644 --- a/packages/medusa-payment-stripe/src/api/routes/hooks/stripe.js +++ b/packages/medusa-payment-stripe/src/api/routes/hooks/stripe.js @@ -24,7 +24,7 @@ export default async (req, res) => { switch (event.type) { case "payment_intent.succeeded": if (order) { - await orderService.update(order._id, { + await orderService.update(order.id, { payment_status: "captured", }) } diff --git a/packages/medusa-payment-stripe/src/services/__mocks__/stripe-provider.js b/packages/medusa-payment-stripe/src/services/__mocks__/stripe-provider.js index aae3c63f42..89fcacb139 100644 --- a/packages/medusa-payment-stripe/src/services/__mocks__/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/__mocks__/stripe-provider.js @@ -10,7 +10,7 @@ export const StripeProviderServiceMock = { } if (payData.id === "pi_no") { return Promise.resolve({ - id: "pi", + id: "pi_no", }) } return Promise.resolve(undefined) diff --git a/packages/medusa-payment-stripe/src/services/__tests__/stripe-provider.js b/packages/medusa-payment-stripe/src/services/__tests__/stripe-provider.js index 1747bf8fc5..f4ec7ac711 100644 --- a/packages/medusa-payment-stripe/src/services/__tests__/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/__tests__/stripe-provider.js @@ -129,6 +129,7 @@ describe("StripeProviderService", () => { result = await stripeProviderService.updatePayment( { id: "pi_lebron", + amount: 800, }, { total: 1000, @@ -136,7 +137,7 @@ describe("StripeProviderService", () => { ) }) - it("returns cancelled stripe payment intent", () => { + it("returns updated stripe payment intent", () => { expect(result).toEqual({ id: "pi_lebron", customer: "cus_lebron", @@ -186,7 +187,13 @@ describe("StripeProviderService", () => { } ) - result = await stripeProviderService.capturePayment("pi_lebron") + result = await stripeProviderService.capturePayment({ + data: { + id: "pi_lebron", + customer: "cus_lebron", + amount: 1000, + }, + }) }) it("returns captured stripe payment intent", () => { @@ -210,7 +217,17 @@ describe("StripeProviderService", () => { } ) - result = await stripeProviderService.refundPayment("pi_lebron", 1000) + result = await stripeProviderService.refundPayment( + { + data: { + id: "re_123", + payment_intent: "pi_lebron", + amount: 1000, + status: "succeeded", + }, + }, + 1000 + ) }) it("returns refunded stripe payment intent", () => { @@ -234,7 +251,13 @@ describe("StripeProviderService", () => { } ) - result = await stripeProviderService.cancelPayment("pi_lebron") + result = await stripeProviderService.cancelPayment({ + data: { + id: "pi_lebron", + customer: "cus_lebron", + status: "cancelled", + }, + }) }) it("returns cancelled stripe payment intent", () => { diff --git a/packages/medusa-payment-stripe/src/services/stripe-provider.js b/packages/medusa-payment-stripe/src/services/stripe-provider.js index ce1b55ca22..b55b921bd2 100644 --- a/packages/medusa-payment-stripe/src/services/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/stripe-provider.js @@ -8,48 +8,78 @@ class StripeProviderService extends PaymentService { constructor({ 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.customerService_ = customerService + /** @private @const {RegionService} */ this.regionService_ = regionService + /** @private @const {TotalsService} */ this.totalsService_ = totalsService } /** - * Status for Stripe PaymentIntent. - * @param {Object} paymentData - payment method data from cart + * 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) { const { id } = paymentData - const paymentIntent = await this.stripe_.paymentIntents.retrieve(id) - let status = "initial" + let status = "pending" if (paymentIntent.status === "requires_payment_method") { return status } + if (paymentIntent.status === "requires_confirmation") { + return status + } + + if (paymentIntent.status === "processing") { + return status + } + + if (paymentIntent.status === "requires_action") { + status = "requires_more" + } + + if (paymentIntent.status === "canceled") { + status = "canceled" + } + if (paymentIntent.status === "requires_capture") { status = "authorized" } if (paymentIntent.status === "succeeded") { - status = "succeeded" - } - - if (paymentIntent.status === "canceled") { - status = "canceled" + status = "authorized" } return status } + /** + * 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) { if (customer.metadata && customer.metadata.stripe_id) { const methods = await this.stripe_.paymentMethods.list({ @@ -63,6 +93,11 @@ class StripeProviderService extends PaymentService { return Promise.resolve([]) } + /** + * Fetches a Stripe customer + * @param {string} customerId - Stripe customer id + * @returns {Promise} Stripe customer + */ async retrieveCustomer(customerId) { if (!customerId) { return Promise.resolve() @@ -70,17 +105,23 @@ class StripeProviderService extends PaymentService { return this.stripe_.customers.retrieve(customerId) } - // customer metadata + /** + * Creates a Stripe customer using a Medusa customer. + * @param {object} customer - Customer data from Medusa + * @returns {Promise} Stripe customer + */ async createCustomer(customer) { try { const stripeCustomer = await this.stripe_.customers.create({ email: customer.email, }) - await this.customerService_.setMetadata( - customer._id, - "stripe_id", - stripeCustomer.id - ) + + if (customer.id) { + await this.customerService_.update(customer.id, { + metadata: { stripe_id: stripeCustomer.id }, + }) + } + return stripeCustomer } catch (error) { throw error @@ -88,50 +129,57 @@ class StripeProviderService extends PaymentService { } /** - * Creates Stripe PaymentIntent. - * @param {string} cart - the cart to create a payment for - * @param {number} amount - the amount to create a payment for - * @returns {string} id of payment intent + * 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 } = cart + const { customer_id, region_id, email } = cart const { currency_code } = await this.regionService_.retrieve(region_id) - let stripeCustomerId - if (!customer_id) { - const { id } = await this.stripe_.customers.create({ - email: cart.email, - }) - stripeCustomerId = id - } else { - const customer = await this.customerService_.retrieve(customer_id) - if (!(customer.metadata && customer.metadata.stripe_id)) { - const { id } = await this.stripe_.customers.create({ - email: customer.email, - }) - await this.customerService_.setMetadata(customer._id, "stripe_id", id) - } else { - stripeCustomerId = customer.metadata.stripe_id - } - } - const amount = await this.totalsService_.getTotal(cart) - const paymentIntent = await this.stripe_.paymentIntents.create({ - customer: stripeCustomerId, - amount: parseInt(amount * 100), // Stripe amount is in cents + + const intentRequest = { + amount: amount, currency: currency_code, setup_future_usage: "on_session", - capture_method: "manual", - metadata: { cart_id: `${cart._id}` }, - }) + capture_method: this.options_.capture ? "automatic" : "manual", + 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 PaymentIntent. + * Retrieves Stripe payment intent. * @param {object} data - the data of the payment to retrieve - * @returns {Object} Stripe PaymentIntent + * @returns {Promise} Stripe payment intent */ async retrievePayment(data) { try { @@ -142,26 +190,74 @@ class StripeProviderService extends PaymentService { } /** - * Updates Stripe PaymentIntent. - * @param {object} data - The payment session data. - * @param {Object} cart - the current cart value - * @returns {Object} Stripe PaymentIntent + * Gets a Stripe payment intent and returns it. + * @param {object} sessionData - the data of the payment to retrieve + * @returns {Promise} Stripe payment intent */ - async updatePayment(data, cart) { + async getPaymentData(sessionData) { try { - const { id } = data - const amount = await this.totalsService_.getTotal(cart) - return this.stripe_.paymentIntents.update(id, { - amount: parseInt(amount * 100), + return this.stripe_.paymentIntents.retrieve(sessionData.data.id) + } catch (error) { + throw error + } + } + + /** + * 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 = {}) { + const stat = await this.getStatus(sessionData.data) + + try { + return { data: sessionData.data, status: stat } + } catch (error) { + throw error + } + } + + async updatePaymentData(sessionData, update) { + try { + return this.stripe_.paymentIntents.update(sessionData.id, { + ...update.data, }) } catch (error) { throw error } } - async deletePayment(data) { + /** + * 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 { id } = data + const stripeId = cart.customer?.metadata?.stripe_id || undefined + + if (stripeId !== sessionData.customer) { + return this.createPayment(cart) + } else { + if (cart.total && sessionData.amount === cart.total) { + return sessionData + } + + return this.stripe_.paymentIntents.update(sessionData.id, { + amount: cart.total, + }) + } + } catch (error) { + throw error + } + } + + async deletePayment(payment) { + try { + const { id } = payment.data return this.stripe_.paymentIntents.cancel(id).catch((err) => { if (err.statusCode === 400) { return @@ -174,15 +270,15 @@ class StripeProviderService extends PaymentService { } /** - * Updates customer of Stripe PaymentIntent. - * @param {string} cart - the cart to update payment intent for - * @param {Object} data - the update object for the payment intent - * @returns {Object} Stripe PaymentIntent + * 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(paymentIntent, id) { + async updatePaymentIntentCustomer(paymentIntentId, customerId) { try { - return this.stripe_.paymentIntents.update(paymentIntent, { - customer: id, + return this.stripe_.paymentIntents.update(paymentIntentId, { + customer: customerId, }) } catch (error) { throw error @@ -190,12 +286,12 @@ class StripeProviderService extends PaymentService { } /** - * Captures payment for Stripe PaymentIntent. - * @param {Object} paymentData - payment method data from cart - * @returns {Object} Stripe PaymentIntent + * Captures payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @returns {object} Stripe payment intent */ - async capturePayment(paymentData) { - const { id } = paymentData + async capturePayment(payment) { + const { id } = payment.data try { return this.stripe_.paymentIntents.capture(id) } catch (error) { @@ -204,32 +300,34 @@ class StripeProviderService extends PaymentService { } /** - * Refunds payment for Stripe PaymentIntent. - * @param {Object} paymentData - payment method data from cart - * @returns {string} id of payment intent + * 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(paymentData, amount) { - const { id } = paymentData + async refundPayment(payment, amountToRefund) { + const { id } = payment.data try { - return this.stripe_.refunds.create({ - amount: parseInt(amount * 100), + await this.stripe_.refunds.create({ + amount: amountToRefund, payment_intent: id, }) + + return payment.data } catch (error) { throw error } } /** - * Cancels payment for Stripe PaymentIntent. - * @param {Object} paymentData - payment method data from cart - * @returns {string} id of payment intent + * Cancels payment for Stripe payment intent. + * @param {object} paymentData - payment method data from cart + * @returns {object} canceled payment intent */ - async cancelPayment(paymentData) { - const { id } = paymentData + async cancelPayment(payment) { + const { id } = payment.data try { - const result = await this.stripe_.paymentIntents.cancel(id) - return result + return this.stripe_.paymentIntents.cancel(id) } catch (error) { if (error.payment_intent.status === "canceled") { return error.payment_intent @@ -241,10 +339,10 @@ class StripeProviderService extends PaymentService { /** * Constructs Stripe Webhook event - * @param {Object} data - the data of the webhook request: req.body - * @param {Object} signature - the Stripe signature on the event, that + * @param {object} data - the data of the webhook request: req.body + * @param {object} signature - the Stripe signature on the event, that * ensures integrity of the webhook event - * @returns {Object} Stripe Webhook event + * @returns {object} Stripe Webhook event */ constructWebhookEvent(data, signature) { return this.stripe_.webhooks.constructEvent( diff --git a/packages/medusa-payment-stripe/src/subscribers/__tests__/cart.js b/packages/medusa-payment-stripe/src/subscribers/__tests__/cart.js deleted file mode 100644 index 9301187a53..0000000000 --- a/packages/medusa-payment-stripe/src/subscribers/__tests__/cart.js +++ /dev/null @@ -1,93 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { carts, CartServiceMock } from "../../__mocks__/cart" -import { CustomerServiceMock } from "../../__mocks__/customer" -import { StripeProviderServiceMock } from "../../services/__mocks__/stripe-provider" -import { EventBusServiceMock } from "../../__mocks__/eventbus" -import CartSubscriber from "../cart" - -describe("CartSubscriber", () => { - describe("onCustomerUpdated", () => { - let cartSubcriber = new CartSubscriber({ - eventBusService: EventBusServiceMock, - cartService: CartServiceMock, - stripeProviderService: StripeProviderServiceMock, - customerService: CustomerServiceMock, - }) - - beforeEach(async () => { - jest.clearAllMocks() - }) - - it("resolves on non-existing payment data", async () => { - await cartSubcriber.onCustomerUpdated(carts.emptyCart) - - expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(0) - }) - - it("cancels old and creates new payment intent with the updated existing customer", async () => { - await cartSubcriber.onCustomerUpdated(carts.frCart) - - expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("lebron") - ) - - expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledTimes(1) - expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledWith( - carts.frCart.payment_sessions[0].data - ) - - expect(StripeProviderServiceMock.cancelPayment).toHaveBeenCalledTimes(1) - expect(StripeProviderServiceMock.cancelPayment).toHaveBeenCalledWith({ - id: "pi", - customer: "cus_123456789", - }) - - expect(StripeProviderServiceMock.createPayment).toHaveBeenCalledTimes(1) - expect(StripeProviderServiceMock.createPayment).toHaveBeenCalledWith( - carts.frCart - ) - - expect(CartServiceMock.updatePaymentSession).toHaveBeenCalledTimes(1) - expect(CartServiceMock.updatePaymentSession).toHaveBeenCalledWith( - IdMap.getId("fr-cart"), - "stripe", - { - id: "pi_new", - customer: "cus_123456789_new", - } - ) - }) - - it("cancels old and creates new payment intent and creates new stripe customer", async () => { - await cartSubcriber.onCustomerUpdated(carts.frCartNoStripeCustomer) - - expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("vvd") - ) - - expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledTimes(1) - expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledWith( - carts.frCartNoStripeCustomer.payment_sessions[0].data - ) - - expect(StripeProviderServiceMock.createCustomer).toHaveBeenCalledTimes(1) - expect(StripeProviderServiceMock.createCustomer).toHaveBeenCalledWith({ - _id: IdMap.getId("vvd"), - first_name: "Virgil", - last_name: "Van Dijk", - email: "virg@vvd.com", - password_hash: "1234", - metadata: {}, - }) - - expect( - StripeProviderServiceMock.updatePaymentIntentCustomer - ).toHaveBeenCalledTimes(1) - expect( - StripeProviderServiceMock.updatePaymentIntentCustomer - ).toHaveBeenCalledWith("cus_123456789_new_vvd") - }) - }) -}) diff --git a/packages/medusa-payment-stripe/src/subscribers/cart.js b/packages/medusa-payment-stripe/src/subscribers/cart.js index c636ac5a0d..e4ccf80d13 100644 --- a/packages/medusa-payment-stripe/src/subscribers/cart.js +++ b/packages/medusa-payment-stripe/src/subscribers/cart.js @@ -2,12 +2,12 @@ class CartSubscriber { constructor({ cartService, customerService, - stripeProviderService, + paymentProviderService, eventBusService, }) { this.cartService_ = cartService this.customerService_ = customerService - this.stripeProviderService_ = stripeProviderService + this.paymentProviderService_ = paymentProviderService this.eventBus_ = eventBusService this.eventBus_.subscribe("cart.customer_updated", async (cart) => { @@ -15,64 +15,37 @@ class CartSubscriber { }) } - async onCustomerUpdated(cart) { - const { customer_id, payment_sessions } = cart + async onCustomerUpdated(cartId) { + const cart = await this.cartService_.retrieve(cartId, { + select: [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "total", + ], + relations: [ + "items", + "billing_address", + "shipping_address", + "region", + "region.payment_providers", + "payment_sessions", + "customer", + ], + }) - if (!payment_sessions) { + if (!cart.payment_sessions?.length) { return Promise.resolve() } - const customer = await this.customerService_.retrieve(customer_id) - - const stripeSession = payment_sessions.find( - (s) => s.provider_id === "stripe" + const session = cart.payment_sessions.find( + (ps) => ps.provider_id === "stripe" ) - if (!stripeSession) { - return Promise.resolve() + if (session) { + return this.paymentProviderService_.updateSession(session, cart) } - - const paymentIntent = await this.stripeProviderService_.retrievePayment( - stripeSession.data - ) - - let stripeCustomer - if (customer.metadata && customer.metadata.stripe_id) { - stripeCustomer = await this.stripeProviderService_.retrieveCustomer( - customer.metadata.stripe_id - ) - } - - if (!stripeCustomer) { - stripeCustomer = await this.stripeProviderService_.createCustomer( - customer - ) - } - - if (stripeCustomer.id === paymentIntent.customer) { - return Promise.resolve() - } - - if (!paymentIntent.customer) { - return this.stripeProviderService_.updatePaymentIntentCustomer( - stripeCustomer.id - ) - } - - if (stripeCustomer.id !== paymentIntent.customer) { - await this.stripeProviderService_.cancelPayment(paymentIntent) - const newPaymentIntent = await this.stripeProviderService_.createPayment( - cart - ) - - await this.cartService_.updatePaymentSession( - cart._id, - "stripe", - newPaymentIntent - ) - } - - return Promise.resolve() } } diff --git a/packages/medusa-plugin-add-ons/package.json b/packages/medusa-plugin-add-ons/package.json index 527ddb15c5..866d97b09a 100644 --- a/packages/medusa-plugin-add-ons/package.json +++ b/packages/medusa-plugin-add-ons/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-add-ons", - "version": "1.0.8", + "version": "1.0.7-alpha.176+0646bd3", "description": "Add-on plugin for Medusa Commerce", "main": "index.js", "repository": { @@ -25,7 +25,7 @@ "cross-env": "^7.0.2", "eslint": "^6.8.0", "jest": "^25.5.2", - "medusa-test-utils": "^1.0.13" + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, "scripts": { "build": "babel src -d . --ignore **/__tests__", @@ -37,8 +37,8 @@ "body-parser": "^1.19.0", "cors": "^2.8.5", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", "redis": "^3.0.2" }, - "gitHead": "3bd91f65304ed1d31c41b85d5c87123450e0542e" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-add-ons/src/services/add-on.js b/packages/medusa-plugin-add-ons/src/services/add-on.js index 7524de0ac9..f4caa33a99 100644 --- a/packages/medusa-plugin-add-ons/src/services/add-on.js +++ b/packages/medusa-plugin-add-ons/src/services/add-on.js @@ -39,16 +39,7 @@ class AddOnService extends BaseService { * @return {string} the validated id */ validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The addOnId could not be casted to an ObjectId" - ) - } - - return value + return rawId } /** @@ -187,7 +178,7 @@ class AddOnService extends BaseService { }) // Return the price if we found a suitable match - if (price) { + if (price !== undefined) { return price } diff --git a/packages/medusa-plugin-brightpearl/package.json b/packages/medusa-plugin-brightpearl/package.json index 4b25a370dc..007e9cb825 100644 --- a/packages/medusa-plugin-brightpearl/package.json +++ b/packages/medusa-plugin-brightpearl/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-brightpearl", - "version": "1.0.26", + "version": "1.0.21-alpha.176+0646bd3", "description": "Brightpearl plugin for Medusa Commerce", "main": "index.js", "repository": { @@ -27,7 +27,7 @@ "cross-env": "^7.0.2", "eslint": "^6.8.0", "jest": "^25.5.2", - "medusa-test-utils": "^1.0.13", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "prettier": "^2.0.5" }, "scripts": { @@ -43,8 +43,8 @@ "axios": "^0.19.2", "axios-rate-limit": "^1.2.1", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", "randomatic": "^3.1.1" }, - "gitHead": "3bd91f65304ed1d31c41b85d5c87123450e0542e" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-brightpearl/src/loaders/token-refresh.js b/packages/medusa-plugin-brightpearl/src/loaders/token-refresh.js deleted file mode 100644 index 5dd3a46b7d..0000000000 --- a/packages/medusa-plugin-brightpearl/src/loaders/token-refresh.js +++ /dev/null @@ -1,25 +0,0 @@ -const REFRESH_CRON = process.env.BP_REFRESH_CRON || "5 4 * * */6" - -const refreshToken = async (container) => { - const logger = container.resolve("logger") - const oauthService = container.resolve("oauthService") - const eventBus = container.resolve("eventBusService") - - try { - logger.info("registering refresh cron job BP") - eventBus.createCronJob("refresh-token-bp", {}, REFRESH_CRON, async () => { - const appData = await oauthService.retrieveByName("brightpearl") - const data = appData.data - if (data && data.refresh_token) { - return oauthService.refreshToken("brightpearl", data.refresh_token) - } - }) - } catch (err) { - if (err.name === "not_allowed") { - return - } - throw err - } -} - -export default refreshToken diff --git a/packages/medusa-plugin-brightpearl/src/services/__tests__/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/__tests__/brightpearl.js index 6d2d2c209b..8f28621da8 100644 --- a/packages/medusa-plugin-brightpearl/src/services/__tests__/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/__tests__/brightpearl.js @@ -6,8 +6,52 @@ import MockAdapter from "axios-mock-adapter" jest.mock("../../utils/brightpearl") +const order = { + region: { + tax_code: "1234", + }, + items: [ + { + title: "Test", + variant: { + sku: "TEST", + }, + unit_price: 1100, + quantity: 2, + }, + ], + shipping_total: 12399, + shipping_methods: [ + { + name: "standard", + price: 12399, + }, + ], + payment_method: { + id: "123", + }, + tax_rate: 23.1, + currency_code: "DKK", + display_id: "1234", + id: "12355", + discounts: [], + shipping_address: { + first_name: "Test", + last_name: "Testson", + address_1: "Test", + address_2: "TEst", + postal_code: "1234", + country_code: "DK", + phone: "12345678", + }, + email: "test@example.com", +} + const OrderService = { - setMetadata: () => { + retrieve: () => { + return Promise.resolve(order) + }, + update: () => { return Promise.resolve() }, } @@ -20,7 +64,7 @@ const TotalsService = { return Promise.resolve([]) }, getShippingTotal: () => { - return 123.9999929393293 + return 12399 }, rounded: (value) => { const decimalPlaces = 4 @@ -91,45 +135,6 @@ describe("BrightpearlService", () => { }) describe("createSalesOrder", () => { - const order = { - items: [ - { - title: "Test", - content: { - variant: { - sku: "TEST", - }, - unit_price: 11, - }, - quantity: 2, - }, - ], - shipping_methods: [ - { - name: "standard", - price: 123.9999929393293, - }, - ], - payment_method: { - _id: "123", - }, - tax_rate: 0.231, - currency_code: "DKK", - display_id: "1234", - _id: "12355", - discounts: [], - shipping_address: { - first_name: "Test", - last_name: "Testson", - address_1: "Test", - address_2: "TEst", - postal_code: "1234", - country_code: "DK", - phone: "12345678", - }, - email: "test@example.com", - } - const bpService = new BrightpearlService( { orderService: OrderService, @@ -187,8 +192,8 @@ describe("BrightpearlService", () => { { name: "Shipping: standard", quantity: 1, - net: 124, - tax: 28.644, + net: 123.99, + tax: 28.6417, taxCode: "1234", nominalCode: "4040", }, diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index 5b2919bda0..c6ca47f89c 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -160,7 +160,7 @@ class BrightpearlService extends BaseService { // Only update if the inventory levels have changed if (parseInt(v.inventory_quantity) !== parseInt(onHand)) { - return this.productVariantService_.update(v._id, { + return this.productVariantService_.update(v.id, { inventory_quantity: onHand, }) } @@ -181,10 +181,13 @@ class BrightpearlService extends BaseService { const onHand = availability[productId].total.onHand const sku = brightpearlProduct.identity.sku - const [variant] = await this.productVariantService_.list({ sku }) + if (!sku) return + const variant = await this.productVariantService_ + .retrieveBySKU(sku) + .catch((_) => undefined) if (variant && variant.manage_inventory) { - await this.productVariantService_.update(variant._id, { + await this.productVariantService_.update(variant.id, { inventory_quantity: onHand, }) } @@ -249,7 +252,7 @@ class BrightpearlService extends BaseService { } async createRefundCredit(fromOrder, fromRefund) { - const region = await this.regionService_.retrieve(fromOrder.region_id) + const region = fromOrder.region const client = await this.getClient() const authData = await this.getAuthData() const orderId = fromOrder.metadata.brightpearl_sales_order_id @@ -266,7 +269,7 @@ class BrightpearlService extends BaseService { const order = { currency: parentSo.currency, ref: parentSo.ref, - externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`, + externalRef: `${parentSo.externalRef}.${fromRefund.id}`, channelId: this.options.channel_id || `1`, installedIntegrationInstanceId: authData.installation_instance_id, customer: parentSo.customer, @@ -277,11 +280,12 @@ class BrightpearlService extends BaseService { name: `${fromRefund.reason}: ${fromRefund.note}`, quantity: 1, taxCode: region.tax_code, - net: this.totalsService_.rounded( - fromRefund.amount / (1 + fromOrder.tax_rate) + net: this.bpnum_( + fromRefund.amount, + 10000 / (100 + fromOrder.tax_rate) ), - tax: this.totalsService_.rounded( - fromRefund.amount - fromRefund.amount / (1 + fromOrder.tax_rate) + tax: this.bpnum_( + fromRefund.amount * (1 - 100 / (100 + fromOrder.tax_rate)) ), nominalCode: accountingCode, }, @@ -291,15 +295,15 @@ class BrightpearlService extends BaseService { return client.orders .createCredit(order) .then(async (creditId) => { - const paymentMethod = fromOrder.payment_method + const paymentMethod = fromOrder.payments[0] const paymentType = "PAYMENT" const payment = { - transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`, - transactionCode: fromOrder._id, + transactionRef: `${paymentMethod.id}.${fromRefund.id}`, + transactionCode: fromOrder.id, paymentMethodCode: this.options.payment_method_code || "1220", orderId: creditId, - currencyIsoCode: fromOrder.currency_code, - amountPaid: fromRefund.amount, + currencyIsoCode: fromOrder.currency_code.toUpperCase(), + amountPaid: this.bpnum_(fromRefund.amount), paymentDate: new Date(), paymentType, } @@ -309,18 +313,21 @@ class BrightpearlService extends BaseService { await client.payments.create(payment) - return this.orderService_.setMetadata( - fromOrder._id, - "brightpearl_credit_ids", - newIds - ) + return this.orderService_.update(fromOrder.id, { + metadata: { + brightpearl_credit_ids: newIds, + }, + }) + }) + .catch((err) => { + console.log(err) + console.log(err.response.data.errors) }) - .catch((err) => console.log(err.response.data.errors)) } } async createSalesCredit(fromOrder, fromReturn) { - const region = await this.regionService_.retrieve(fromOrder.region_id) + const region = fromOrder.region const client = await this.getClient() const authData = await this.getAuthData() const orderId = fromOrder.metadata.brightpearl_sales_order_id @@ -329,7 +336,7 @@ class BrightpearlService extends BaseService { const order = { currency: parentSo.currency, ref: parentSo.ref, - externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`, + externalRef: `${parentSo.externalRef}.${fromReturn.id}`, channelId: this.options.channel_id || `1`, installedIntegrationInstanceId: authData.installation_instance_id, customer: parentSo.customer, @@ -359,18 +366,14 @@ class BrightpearlService extends BaseService { return acc + next.net + next.tax }, 0) - const difference = fromReturn.refund_amount - total + const difference = (fromReturn.refund_amount / 100 - total) * 100 if (difference) { order.rows.push({ name: "Difference", quantity: 1, taxCode: region.tax_code, - net: this.totalsService_.rounded( - difference / (1 + fromOrder.tax_rate) - ), - tax: this.totalsService_.rounded( - difference - difference / (1 + fromOrder.tax_rate) - ), + net: this.bpnum_(difference, 10000 / (100 + fromOrder.tax_rate)), + tax: this.bpnum_(difference * (1 - 100 / (100 + fromOrder.tax_rate))), nominalCode: this.options.sales_account_code || "4000", }) } @@ -378,15 +381,15 @@ class BrightpearlService extends BaseService { return client.orders .createCredit(order) .then(async (creditId) => { - const paymentMethod = fromOrder.payment_method + const paymentMethod = fromOrder.payments[0] const paymentType = "PAYMENT" const payment = { - transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`, - transactionCode: fromOrder._id, + transactionRef: `${paymentMethod.id}.${fromReturn.id}`, + transactionCode: fromOrder.id, paymentMethodCode: this.options.payment_method_code || "1220", orderId: creditId, - currencyIsoCode: fromOrder.currency_code, - amountPaid: fromReturn.refund_amount, + currencyIsoCode: fromOrder.currency_code.toUpperCase(), + amountPaid: this.bpnum_(fromReturn.refund_amount), paymentDate: new Date(), paymentType, } @@ -396,17 +399,34 @@ class BrightpearlService extends BaseService { await client.payments.create(payment) - return this.orderService_.setMetadata( - fromOrder._id, - "brightpearl_credit_ids", - newIds - ) + return this.orderService_.update(fromOrder.id, { + metadata: { + brightpearl_credit_ids: newIds, + }, + }) }) - .catch((err) => console.log(err.response.data.errors)) + .catch((err) => console.log(err)) } } - async createSalesOrder(fromOrder) { + async createSalesOrder(fromOrderId) { + const fromOrder = await this.orderService_.retrieve(fromOrderId, { + select: [ + "total", + "subtotal", + "shipping_total", + "gift_card_total", + "discount_total", + ], + relations: [ + "region", + "shipping_address", + "billing_address", + "shipping_methods", + "payments", + ], + }) + const client = await this.getClient() let customer = await this.retrieveCustomerByEmail(fromOrder.email) @@ -420,10 +440,10 @@ class BrightpearlService extends BaseService { const { shipping_address } = fromOrder const order = { currency: { - code: fromOrder.currency_code, + code: fromOrder.currency_code.toUpperCase(), }, ref: fromOrder.display_id, - externalRef: fromOrder._id, + externalRef: fromOrder.id, channelId: this.options.channel_id || `1`, installedIntegrationInstanceId: authData.installation_instance_id, statusId: this.options.default_status_id || `3`, @@ -464,33 +484,34 @@ class BrightpearlService extends BaseService { return salesOrderId }) .then((salesOrderId) => { - return this.orderService_.setMetadata( - fromOrder._id, - "brightpearl_sales_order_id", - salesOrderId - ) + return this.orderService_.update(fromOrder.id, { + metadata: { + brightpearl_sales_order_id: salesOrderId, + }, + }) }) } - async createSwapPayment(fromSwap) { + async createSwapPayment(fromSwapId) { + const fromSwap = await this.swapService_.retrieve(fromSwapId) + const client = await this.getClient() const soId = fromSwap.metadata && fromSwap.metadata.brightpearl_sales_order_id - if (!soId || fromSwap.amount_paid <= 0) { + if (!soId || fromSwap.difference_due <= 0) { return } const paymentType = "RECEIPT" - const paymentMethod = fromSwap.payment_method const payment = { - transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref - transactionCode: fromSwap._id, + transactionRef: `${fromSwap.id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref + transactionCode: fromSwap.id, paymentMethodCode: this.options.payment_method_code || "1220", orderId: soId, paymentDate: new Date(), - currencyIsoCode: fromSwap.currency_code, - amountPaid: fromSwap.amount_paid, + currencyIsoCode: fromSwap.order.currency_code.toUpperCase(), + amountPaid: fromSwap.difference_due, paymentType, } @@ -502,20 +523,23 @@ class BrightpearlService extends BaseService { let customer = await this.retrieveCustomerByEmail(fromOrder.email) if (!customer) { - customer = await this.createCustomer(fromOrder) + customer = await this.createCustomer({ + ...fromOrder, + ...fromSwap, + }) } const authData = await this.getAuthData() - const sIndex = fromOrder.swaps.findIndex((s) => fromSwap._id.equals(s)) + const sIndex = fromOrder.swaps.findIndex((s) => fromSwap.id === s.id) const { shipping_address } = fromSwap const order = { currency: { - code: fromOrder.currency_code, + code: fromOrder.currency_code.toUpperCase(), }, ref: `${fromOrder.display_id}-S${sIndex + 1}`, - externalRef: `${fromOrder._id}.${fromSwap._id}`, + externalRef: `${fromOrder.id}.${fromSwap.id}`, channelId: this.options.channel_id || `1`, installedIntegrationInstanceId: authData.installation_instance_id, statusId: @@ -545,7 +569,7 @@ class BrightpearlService extends BaseService { }, }, rows: await this.getBrightpearlRows({ - region_id: fromOrder.region_id, + region: fromOrder.region, discounts: fromOrder.discounts, tax_rate: fromOrder.tax_rate, items: fromSwap.additional_items, @@ -562,14 +586,14 @@ class BrightpearlService extends BaseService { return acc + parseFloat(next.net) + parseFloat(next.tax) }, 0) - const paymentMethod = fromOrder.payment_method + const paymentMethod = fromOrder.payments[0] const paymentType = "RECEIPT" const payment = { - transactionRef: `${paymentMethod._id}.${paymentType}-${fromSwap._id}`, - transactionCode: fromOrder._id, + transactionRef: `${paymentMethod.id}.${paymentType}-${fromSwap.id}`, + transactionCode: fromOrder.id, paymentMethodCode: this.options.payment_method_code || "1220", orderId: salesOrderId, - currencyIsoCode: fromOrder.currency_code, + currencyIsoCode: fromOrder.currency_code.toUpperCase(), amountPaid: total, paymentDate: new Date(), paymentType, @@ -577,33 +601,33 @@ class BrightpearlService extends BaseService { await client.payments.create(payment) - return this.swapService_.setMetadata( - fromSwap._id, - "brightpearl_sales_order_id", - salesOrderId - ) + return this.swapService_.update(fromSwap.id, { + metadata: { + brightpearl_sales_order_id: salesOrderId, + }, + }) }) } async createSwapCredit(fromOrder, fromSwap) { - const region = await this.regionService_.retrieve(fromOrder.region_id) + const region = fromOrder.region const client = await this.getClient() const authData = await this.getAuthData() const orderId = fromOrder.metadata.brightpearl_sales_order_id - const sIndex = fromOrder.swaps.findIndex((s) => fromSwap._id.equals(s)) + const sIndex = fromOrder.swaps.findIndex((s) => fromSwap.id === s.id) if (orderId) { const parentSo = await client.orders.retrieve(orderId) const order = { currency: parentSo.currency, ref: `${parentSo.ref}-S${sIndex + 1}`, - externalRef: `${parentSo.externalRef}.${fromSwap._id}`, + externalRef: `${parentSo.externalRef}.${fromSwap.id}`, channelId: this.options.channel_id || `1`, installedIntegrationInstanceId: authData.installation_instance_id, customer: parentSo.customer, delivery: parentSo.delivery, parentId: orderId, - rows: fromSwap.return.items.map((i) => { + rows: fromSwap.return_order.items.map((i) => { const parentRow = parentSo.rows.find((row) => { return row.externalRef === i.item_id }) @@ -623,13 +647,18 @@ class BrightpearlService extends BaseService { }), } - if (fromSwap.return_shipping && fromSwap.return_shipping.price) { + if ( + fromSwap.return_order.shipping_method && + fromSwap.return_order.shipping_method.price + ) { order.rows.push({ name: "Return Shipping", quantity: 1, taxCode: region.tax_code, - net: -1 * fromSwap.return_shipping.price, - tax: -1 * fromSwap.return_shipping.price * fromOrder.tax_rate, + net: (-1 * fromSwap.return_order.shipping_method.price) / 100, + tax: + ((-1 * fromSwap.return_order.shipping_method.price) / 100) * + (fromOrder.tax_rate / 100), nominalCode: this.options.shipping_account_code || "4040", }) } @@ -641,14 +670,14 @@ class BrightpearlService extends BaseService { return client.orders .createCredit(order) .then(async (creditId) => { - const paymentMethod = fromOrder.payment_method + const paymentMethod = fromOrder.payments[0] const paymentType = "PAYMENT" const payment = { - transactionRef: `${paymentMethod._id}.${paymentType}-${fromSwap._id}`, - transactionCode: fromSwap._id, + transactionRef: `${paymentMethod.id}.${paymentType}-${fromSwap.id}`, + transactionCode: fromSwap.id, paymentMethodCode: this.options.payment_method_code || "1220", orderId: creditId, - currencyIsoCode: fromSwap.currency_code, + currencyIsoCode: fromOrder.currency_code.toUpperCase(), amountPaid: total, paymentDate: new Date(), paymentType, @@ -660,7 +689,12 @@ class BrightpearlService extends BaseService { } } - async createPayment(fromOrder) { + async createPayment(fromOrderId) { + const fromOrder = await this.orderService_.retrieve(fromOrderId, { + select: ["total"], + relations: ["payments"], + }) + const client = await this.getClient() const soId = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id @@ -669,38 +703,35 @@ class BrightpearlService extends BaseService { } const paymentType = "RECEIPT" - const paymentMethod = fromOrder.payment_method const payment = { - transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref - transactionCode: fromOrder._id, + transactionRef: `${fromOrder.id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref + transactionCode: fromOrder.id, paymentMethodCode: this.options.payment_method_code || "1220", orderId: soId, paymentDate: new Date(), - currencyIsoCode: fromOrder.currency_code, - amountPaid: await this.totalsService_.getTotal(fromOrder), + currencyIsoCode: fromOrder.currency_code.toUpperCase(), + amountPaid: this.bpnum_(fromOrder.total), paymentType, } - await client.payments.create(payment) + return client.payments.create(payment) } async getBrightpearlRows(fromOrder) { - const region = await this.regionService_.retrieve(fromOrder.region_id) + const { region } = fromOrder const discount = fromOrder.discounts.find( - ({ discount_rule }) => discount_rule.type !== "free_shipping" + ({ rule }) => rule.type !== "free_shipping" ) let lineDiscounts = [] - if (discount && !discount.is_giftcard) { + if (discount) { lineDiscounts = this.totalsService_.getLineDiscounts(fromOrder, discount) } const lines = await Promise.all( fromOrder.items.map(async (item) => { - const bpProduct = await this.retrieveProductBySKU( - item.content.variant.sku - ) + const bpProduct = await this.retrieveProductBySKU(item.variant.sku) - const ld = lineDiscounts.find((l) => item._id === l.item._id) || { + const ld = lineDiscounts.find((l) => item.id === l.item.id) || { amount: 0, } @@ -710,13 +741,14 @@ class BrightpearlService extends BaseService { } else { row.name = item.title } - row.net = this.totalsService_.rounded( - item.content.unit_price * item.quantity - ld.amount + row.net = this.bpnum_(item.unit_price * item.quantity - ld.amount) + row.tax = this.bpnum_( + item.unit_price * item.quantity - ld.amount, + fromOrder.tax_rate ) - row.tax = this.totalsService_.rounded(row.net * fromOrder.tax_rate) row.quantity = item.quantity row.taxCode = region.tax_code - row.externalRef = item._id + row.externalRef = item.id row.nominalCode = this.options.sales_account_code || "4000" if (item.is_giftcard) { @@ -731,30 +763,28 @@ class BrightpearlService extends BaseService { // correspondingly. This reduces the amount payable, while debiting the // gift card account that was previously credited, when the gift card was // purchased. - if (discount && discount.is_giftcard) { - const discountTotal = await this.totalsService_.getDiscountTotal( - fromOrder - ) + const gcTotal = fromOrder.gift_card_total + if (gcTotal) { lines.push({ - name: `Gift Card: ${discount.code}`, - net: -1 * discountTotal, - tax: this.totalsService_.rounded( - -1 * discountTotal * fromOrder.tax_rate - ), + name: `Gift Card`, + net: this.bpnum_(-1 * gcTotal), + tax: this.bpnum_(-1 * gcTotal, fromOrder.tax_rate), quantity: 1, taxCode: region.tax_code, nominalCode: this.options.gift_card_account_code || "4000", }) } - const shippingTotal = this.totalsService_.getShippingTotal(fromOrder) + const shippingTotal = + fromOrder.shipping_total || + this.totalsService_.getShippingTotal(fromOrder) const shippingMethods = fromOrder.shipping_methods if (shippingMethods.length > 0) { lines.push({ name: `Shipping: ${shippingMethods.map((m) => m.name).join(" + ")}`, quantity: 1, - net: this.totalsService_.rounded(shippingTotal), - tax: this.totalsService_.rounded(shippingTotal * fromOrder.tax_rate), + net: this.bpnum_(shippingTotal), + tax: this.bpnum_(shippingTotal, fromOrder.tax_rate), taxCode: region.tax_code, nominalCode: this.options.shipping_account_code || "4040", }) @@ -821,11 +851,10 @@ class BrightpearlService extends BaseService { .filter((i) => !!i) // Orders with a concatenated externalReference are swap orders - const [orderId, swapId] = order.externalRef.split(".") + const [_, swapId] = order.externalRef.split(".") if (swapId) { - const order = await this.orderService_.retrieve(orderId) - return this.swapService_.createFulfillment(order, swapId, { + return this.swapService_.createFulfillment(swapId, { goods_out_note: id, }) } @@ -866,6 +895,11 @@ class BrightpearlService extends BaseService { return { contactId: customer } } + + bpnum_(number, taxRate = 100) { + const bpNumber = number / 100 + return this.totalsService_.rounded(bpNumber * (taxRate / 100)) + } } export default BrightpearlService diff --git a/packages/medusa-plugin-brightpearl/src/subscribers/order.js b/packages/medusa-plugin-brightpearl/src/subscribers/order.js index e26c5499c8..2d54c3e51e 100644 --- a/packages/medusa-plugin-brightpearl/src/subscribers/order.js +++ b/packages/medusa-plugin-brightpearl/src/subscribers/order.js @@ -3,18 +3,24 @@ class OrderSubscriber { eventBusService, orderService, swapService, + returnService, + paymentProviderService, brightpearlService, + fulfillmentService, }) { this.orderService_ = orderService this.brightpearlService_ = brightpearlService this.swapService_ = swapService + this.returnService_ = returnService + this.paymentProviderService_ = paymentProviderService + this.fulfillmentService_ = fulfillmentService + + eventBusService.subscribe("order.placed", this.sendToBrightpearl) eventBusService.subscribe("order.refund_created", this.registerRefund) eventBusService.subscribe("order.items_returned", this.registerReturn) - eventBusService.subscribe("order.placed", this.sendToBrightpearl) - eventBusService.subscribe( "order.payment_captured", this.registerCapturedPayment @@ -24,50 +30,65 @@ class OrderSubscriber { eventBusService.subscribe("swap.shipment_created", this.registerShipment) // Before we initiate a swap we wait for the payment and the return - eventBusService.subscribe("swap.payment_completed", this.registerSwap) + eventBusService.subscribe( + "swap.payment_completed", + this.registerSwapPayment + ) eventBusService.subscribe("order.swap_received", this.registerSwap) } - sendToBrightpearl = (order) => { - return this.brightpearlService_.createSalesOrder(order) + sendToBrightpearl = (data) => { + return this.brightpearlService_.createSalesOrder(data.id) } - registerCapturedPayment = (order) => { - return this.brightpearlService_.createPayment(order) + registerCapturedPayment = ({ id }) => { + return this.brightpearlService_.createPayment(id) + } + + registerSwapPayment = async (data) => { + return this.registerSwap({ id: data.id, swap_id: data.id }) } registerSwap = async (data) => { - const { order, swap, swap_id } = data + const { id, swap_id } = data - if (!order && !swap) { + if (!id && !swap_id) { return } - let fromOrder = order - if (!fromOrder) { - fromOrder = await this.orderService_.retrieve(swap.order_id) - } + const fromSwap = await this.swapService_.retrieve(swap_id, { + relations: [ + "order", + "order.payments", + "order.region", + "order.swaps", + "order.discounts", + "return_order", + "return_order.items", + "return_order.shipping_method", + "additional_items", + "shipping_address", + "shipping_methods", + ], + }) + let fromOrder = fromSwap.order - let fromSwap - if (swap) { - fromSwap = await this.swapService_.retrieve(swap._id) - } else { - fromSwap = await this.swapService_.retrieve(swap_id) - } - - if (!(fromSwap.is_paid && fromSwap.return.status === "received")) { + if ( + !( + fromSwap.confirmed_at !== null && + fromSwap.return_order.status === "received" + ) + ) { return } await this.brightpearlService_.createSwapCredit(fromOrder, fromSwap) await this.brightpearlService_.createSwapOrder(fromOrder, fromSwap) - - const paySwap = await this.swapService_.retrieve(fromSwap._id) - await this.brightpearlService_.createSwapPayment(paySwap) } registerShipment = async (data) => { - const { shipment } = data + const { fulfillment_id } = data + const shipment = await this.fulfillmentService_.retrieve(fulfillment_id) const noteId = shipment.metadata.goods_out_note if (noteId) { await this.brightpearlService_.registerGoodsOutTrackingNumber( @@ -78,15 +99,28 @@ class OrderSubscriber { } } - registerReturn = (data) => { - const { order, return: fromReturn } = data + registerReturn = async (data) => { + const { id, return_id } = data + + const order = await this.orderService_.retrieve(id, { + relations: ["region", "payments"], + }) + + const fromReturn = await this.returnService_.retrieve(return_id, { + relations: ["items"], + }) + return this.brightpearlService_ .createSalesCredit(order, fromReturn) .catch((err) => console.log(err)) } - registerRefund = (data) => { - const { order, refund } = data + registerRefund = async (data) => { + const { id, refund_id } = data + const order = await this.orderService_.retrieve(id, { + relations: ["region", "payments"], + }) + const refund = await this.paymentProviderService_.retrieveRefund(refund_id) return this.brightpearlService_ .createRefundCredit(order, refund) .catch((err) => console.log(err)) diff --git a/packages/medusa-plugin-contentful/.npmignore b/packages/medusa-plugin-contentful/.npmignore index 486581be18..73122644c5 100644 --- a/packages/medusa-plugin-contentful/.npmignore +++ b/packages/medusa-plugin-contentful/.npmignore @@ -5,5 +5,9 @@ node_modules /*.js !index.js yarn.lock - +src +.gitignore +.eslintrc +.babelrc +.prettierrc diff --git a/packages/medusa-plugin-contentful/package.json b/packages/medusa-plugin-contentful/package.json index e59a11d347..0de21b5512 100644 --- a/packages/medusa-plugin-contentful/package.json +++ b/packages/medusa-plugin-contentful/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-contentful", - "version": "1.0.15", + "version": "1.0.12-alpha.176+0646bd3", "description": "Contentful plugin for Medusa Commerce", "main": "index.js", "repository": { @@ -39,9 +39,9 @@ "body-parser": "^1.19.0", "contentful-management": "^5.27.1", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "redis": "^3.0.2" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-contentful/src/services/contentful.js b/packages/medusa-plugin-contentful/src/services/contentful.js index 5b879cef59..51b2f6eabe 100644 --- a/packages/medusa-plugin-contentful/src/services/contentful.js +++ b/packages/medusa-plugin-contentful/src/services/contentful.js @@ -46,21 +46,16 @@ class ContentfulService extends BaseService { } } - async getVariantEntries_(productId) { + async getVariantEntries_(variants) { try { - const productVariants = await this.productService_.retrieveVariants( - productId - ) - const contentfulVariants = await Promise.all( - productVariants.map((variant) => + variants.map((variant) => this.updateProductVariantInContentful(variant) ) ) return contentfulVariants } catch (error) { - console.log(error) throw error } } @@ -118,79 +113,33 @@ class ContentfulService extends BaseService { async createProductInContentful(product) { try { + const p = await this.productService_.retrieve(product.id, { + relations: ["variants", "options"], + }) + const environment = await this.getContentfulEnvironment_() - const variantEntries = await this.getVariantEntries_(product._id) + const variantEntries = await this.getVariantEntries_(p.variants) const variantLinks = this.getVariantLinks_(variantEntries) - const fieldsObject = { - title: { - "en-US": product.title, - }, - variants: { - "en-US": variantLinks, - }, - options: { - "en-US": product.options, - }, - objectId: { - "en-US": product._id, - }, - } - - if (product.images.length > 0) { - const imageLinks = await this.createImageAssets(product) - - const thumbnailAsset = await environment.createAsset({ - fields: { - title: { - "en-US": `${product.title}`, - }, - description: { - "en-US": "", - }, - file: { - "en-US": { - contentType: "image/xyz", - fileName: product.thumbnail, - upload: product.thumbnail, - }, - }, + const result = await environment.createEntryWithId("product", p.id, { + fields: { + title: { + "en-US": p.title, }, - }) - - await thumbnailAsset.processForAllLocales() - - const thumbnailLink = { - sys: { - type: "Link", - linkType: "Asset", - id: thumbnailAsset.sys.id, + variants: { + "en-US": variantLinks, }, - } - - fieldsObject.thumbnail = { - "en-US": thumbnailLink, - } - - if (imageLinks) { - fieldsObject.images = { - "en-US": imageLinks, - } - } - } - - const result = await environment.createEntryWithId( - "product", - product._id, - { - fields: { - ...fieldsObject, + options: { + "en-US": p.options, }, - } - ) + objectId: { + "en-US": p.id, + }, + }, + }) const ignoreIds = (await this.getIgnoreIds_("product")) || [] - ignoreIds.push(product._id) + ignoreIds.push(product.id) this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds)) return result } catch (error) { @@ -200,33 +149,37 @@ class ContentfulService extends BaseService { async createProductVariantInContentful(variant) { try { + const v = await this.productVariantService_.retrieve(variant.id, { + relations: ["prices", "options"], + }) + const environment = await this.getContentfulEnvironment_() const result = await environment.createEntryWithId( "productVariant", - variant._id, + v.id, { fields: { title: { - "en-US": variant.title, + "en-US": v.title, }, sku: { - "en-US": variant.sku, + "en-US": v.sku, }, prices: { - "en-US": variant.prices, + "en-US": v.prices, }, options: { - "en-US": variant.options, + "en-US": v.options, }, objectId: { - "en-US": variant._id, + "en-US": v.id, }, }, } ) const ignoreIds = (await this.getIgnoreIds_("product_variant")) || [] - ignoreIds.push(variant._id) + ignoreIds.push(v.id) this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds)) return result } catch (error) { @@ -238,12 +191,12 @@ class ContentfulService extends BaseService { try { const ignoreIds = (await this.getIgnoreIds_("product")) || [] - if (ignoreIds.includes(product._id)) { - const newIgnoreIds = ignoreIds.filter((id) => id !== product._id) + if (ignoreIds.includes(product.id)) { + const newIgnoreIds = ignoreIds.filter((id) => id !== product.id) this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds)) return } else { - ignoreIds.push(product._id) + ignoreIds.push(product.id) this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds)) } @@ -251,27 +204,31 @@ class ContentfulService extends BaseService { // check if product exists let productEntry = undefined try { - productEntry = await environment.getEntry(product._id) + productEntry = await environment.getEntry(product.id) } catch (error) { return this.createProductInContentful(product) } - const variantEntries = await this.getVariantEntries_(product._id) + const p = await this.productService_.retrieve(product.id, { + relations: ["options", "variants"], + }) + + const variantEntries = await this.getVariantEntries_(p.variants) const variantLinks = this.getVariantLinks_(variantEntries) const productEntryFields = { ...productEntry.fields, title: { - "en-US": product.title, + "en-US": p.title, }, options: { - "en-US": product.options, + "en-US": p.options, }, variants: { "en-US": variantLinks, }, objectId: { - "en-US": product._id, + "en-US": p.id, }, } @@ -290,15 +247,15 @@ class ContentfulService extends BaseService { try { const ignoreIds = (await this.getIgnoreIds_("product_variant")) || [] - if (ignoreIds.includes(variant._id)) { - const newIgnoreIds = ignoreIds.filter((id) => id !== variant._id) + if (ignoreIds.includes(variant.id)) { + const newIgnoreIds = ignoreIds.filter((id) => id !== variant.id) this.redis_.set( "product_variant_ignore_ids", JSON.stringify(newIgnoreIds) ) return } else { - ignoreIds.push(variant._id) + ignoreIds.push(variant.id) this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds)) } @@ -307,32 +264,31 @@ class ContentfulService extends BaseService { let variantEntry = undefined // if not, we create a new one try { - variantEntry = await environment.getEntry(variant._id) + variantEntry = await environment.getEntry(variant.id) } catch (error) { return this.createProductVariantInContentful(variant) } - const cleanPrices = variant.prices.map((price) => ({ - ...price, - sale_amount: price.sale_amount || undefined, - })) + const v = await this.productVariantService_.retrieve(variant.id, { + relations: ["prices", "options"], + }) const variantEntryFields = { ...variantEntry.fields, title: { - "en-US": variant.title, + "en-US": v.title, }, sku: { - "en-US": variant.sku, + "en-US": v.sku, }, options: { - "en-US": variant.options, + "en-US": v.options, }, prices: { - "en-US": cleanPrices, + "en-US": v.prices, }, objectId: { - "en-US": variant._id, + "en-US": v.id, }, } diff --git a/packages/medusa-plugin-contentful/src/subscribers/contentful.js b/packages/medusa-plugin-contentful/src/subscribers/contentful.js index 7add5c474b..301ac00160 100644 --- a/packages/medusa-plugin-contentful/src/subscribers/contentful.js +++ b/packages/medusa-plugin-contentful/src/subscribers/contentful.js @@ -1,5 +1,12 @@ class ContentfulSubscriber { - constructor({ contentfulService, eventBusService }) { + constructor({ + contentfulService, + productVariantService, + productService, + eventBusService, + }) { + this.productVariantService_ = productVariantService + this.productService_ = productService this.contentfulService_ = contentfulService this.eventBus_ = eventBusService diff --git a/packages/medusa-plugin-discount-generator/package.json b/packages/medusa-plugin-discount-generator/package.json index a14f284069..cc5a2e2e42 100644 --- a/packages/medusa-plugin-discount-generator/package.json +++ b/packages/medusa-plugin-discount-generator/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-discount-generator", - "version": "1.0.1", + "version": "1.0.2-alpha.787+0646bd3", "main": "index.js", "license": "MIT", "author": "Sebastian Rindom", @@ -40,5 +40,5 @@ "medusa-core-utils": "^0.1.27", "randomatic": "^3.1.1" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-economic/package.json b/packages/medusa-plugin-economic/package.json index 152c3bcfca..613d1852c2 100644 --- a/packages/medusa-plugin-economic/package.json +++ b/packages/medusa-plugin-economic/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-economic", - "version": "1.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "E-conomic financial reporting", "main": "index.js", "repository": { @@ -39,9 +39,9 @@ "axios": "^0.19.2", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "moment": "^2.27.0" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-economic/src/services/economic.js b/packages/medusa-plugin-economic/src/services/economic.js index a3f65ceab1..3c7fbf40ca 100644 --- a/packages/medusa-plugin-economic/src/services/economic.js +++ b/packages/medusa-plugin-economic/src/services/economic.js @@ -71,7 +71,7 @@ class EconomicService extends BaseService { let order_lines = [] // Find the discount, that is not free shipping const discount = order.discounts.find( - ({ discount_rule }) => discount_rule.type !== "free_shipping" + ({ rule }) => rule.type !== "free_shipping" ) // If the discount has an item specific allocation method, // we need to fetch the discount for each item diff --git a/packages/medusa-plugin-ip-lookup/.npmignore b/packages/medusa-plugin-ip-lookup/.npmignore index 486581be18..73122644c5 100644 --- a/packages/medusa-plugin-ip-lookup/.npmignore +++ b/packages/medusa-plugin-ip-lookup/.npmignore @@ -5,5 +5,9 @@ node_modules /*.js !index.js yarn.lock - +src +.gitignore +.eslintrc +.babelrc +.prettierrc diff --git a/packages/medusa-plugin-ip-lookup/package.json b/packages/medusa-plugin-ip-lookup/package.json index 27691df7b6..975e5e79fb 100644 --- a/packages/medusa-plugin-ip-lookup/package.json +++ b/packages/medusa-plugin-ip-lookup/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-ip-lookup", - "version": "1.0.11", + "version": "1.0.12-alpha.787+0646bd3", "description": "IP lookup middleware for Medusa core", "main": "dist/index.js", "repository": { @@ -35,5 +35,5 @@ "axios": "^0.20.0", "mongoose": "^5.8.0" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-ip-lookup/src/api/medusa-middleware.js b/packages/medusa-plugin-ip-lookup/src/api/medusa-middleware.js index a602b678bf..6853543085 100644 --- a/packages/medusa-plugin-ip-lookup/src/api/medusa-middleware.js +++ b/packages/medusa-plugin-ip-lookup/src/api/medusa-middleware.js @@ -7,7 +7,8 @@ export default { } const ipLookupService = req.scope.resolve("ipLookupService") - const regionService = req.scope.resolve("regionService") + const manager = req.scope.resolve("manager") + const countryRepository = req.scope.resolve("countryRepository") const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress @@ -18,15 +19,15 @@ export default { return } - // Find region using the country code from ip lookup - const regions = await regionService.list({ - countries: data.country_code, + const countryRepo = manager.getCustomRepository(countryRepository) + const country = await countryRepo.findOne({ + where: { iso_2: data.country_code.toLowerCase() }, }) - // If this region exists, add it to the body of the cart creation request - if (regions[0]) { - req.body.region_id = regions[0]._id.toString() - req.body.country_code = data.country_code + // If country exists, add it to the body of the cart creation request + if (country) { + req.body.region_id = country.region_id + req.body.country_code = country.iso_2 } next() diff --git a/packages/medusa-plugin-mailchimp/package.json b/packages/medusa-plugin-mailchimp/package.json index fe092964af..98bfbdb168 100644 --- a/packages/medusa-plugin-mailchimp/package.json +++ b/packages/medusa-plugin-mailchimp/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-mailchimp", - "version": "1.0.15", + "version": "1.0.14-alpha.176+0646bd3", "description": "Mailchimp newsletter subscriptions", "main": "index.js", "repository": { @@ -40,7 +40,8 @@ "cors": "^2.8.5", "express": "^4.17.1", "mailchimp-api-v3": "^1.14.0", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13" - } + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" + }, + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-permissions/package.json b/packages/medusa-plugin-permissions/package.json index e20816e601..8330ae67b6 100644 --- a/packages/medusa-plugin-permissions/package.json +++ b/packages/medusa-plugin-permissions/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-permissions", - "version": "1.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "Role permission for Medusa core", "main": "dist/index.js", "repository": { @@ -32,9 +32,9 @@ "medusa-interfaces": "1.x" }, "dependencies": { - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "mongoose": "^5.8.0" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-segment/package.json b/packages/medusa-plugin-segment/package.json index cd8b4a2ed6..e75fd8ae8f 100644 --- a/packages/medusa-plugin-segment/package.json +++ b/packages/medusa-plugin-segment/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-segment", - "version": "1.0.16", + "version": "1.0.14-alpha.176+0646bd3", "description": "Segment Analytics", "main": "index.js", "repository": { @@ -39,8 +39,8 @@ "axios": "^0.19.2", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-segment/src/services/segment.js b/packages/medusa-plugin-segment/src/services/segment.js index 171131896c..c270723e9e 100644 --- a/packages/medusa-plugin-segment/src/services/segment.js +++ b/packages/medusa-plugin-segment/src/services/segment.js @@ -13,8 +13,8 @@ class SegmentService extends BaseService { constructor({ totalsService }, options) { super() - this.options_ = options this.totalsService_ = totalsService + this.options_ = options this.analytics_ = new Analytics(options.write_key) } @@ -30,14 +30,17 @@ class SegmentService extends BaseService { return this.analytics_.track(data) } - async getReportingValue(fromCurrency, value) { + async getReportingValue(rawCurrency, value) { + const fromCurrency = rawCurrency.toUpperCase() const date = "latest" const toCurrency = (this.options_.reporting_currency && this.options_.reporting_currency.toUpperCase()) || "EUR" - if (fromCurrency === toCurrency) return value.toFixed(2) + if (fromCurrency === toCurrency) { + return this.totalsService_.rounded(value) + } const exchangeRate = await axios .get( @@ -47,28 +50,28 @@ class SegmentService extends BaseService { return data.rates[fromCurrency] }) - return (value / exchangeRate).toFixed(2) + return this.totalsService_.rounded(value / exchangeRate) } async buildOrder(order) { - const subtotal = await this.totalsService_.getSubtotal(order) - const total = await this.totalsService_.getTotal(order) - const tax = await this.totalsService_.getTaxTotal(order) - const discount = await this.totalsService_.getDiscountTotal(order) - const shipping = await this.totalsService_.getShippingTotal(order) + const subtotal = order.subtotal / 100 + const total = order.total / 100 + const tax = order.tax_total / 100 + const discount = order.discount_total / 100 + const shipping = order.shipping_total / 100 const revenue = total - tax let coupon if (order.discounts && order.discounts.length) { - coupon = order.discounts[0].code + coupon = order.discounts[0] && order.discounts[0].code } const orderData = { checkout_id: order.cart_id, - order_id: order._id, + order_id: order.id, email: order.email, region_id: order.region_id, - payment_provider: order.payment_method.provider_id, + payment_provider: order.payments.map((p) => p.provider_id).join(","), shipping_methods: order.shipping_methods, shipping_country: order.shipping_address.country_code, shipping_city: order.shipping_address.city, @@ -102,8 +105,8 @@ class SegmentService extends BaseService { order.items.map(async (item) => { let name = item.title - const unit_price = item.content.unit_price - const line_total = unit_price * item.content.quantity * item.quantity + const unit_price = item.unit_price + const line_total = (unit_price * item.quantity) / 100 const revenue = await this.getReportingValue( order.currency_code, line_total @@ -114,11 +117,11 @@ class SegmentService extends BaseService { return { name, - variant: item.content.variant.sku, - price: unit_price, + variant, + price: unit_price / 100, reporting_revenue: revenue, - product_id: `${item.content.product._id}`, - sku: skuParts.join("-"), + product_id: item.variant.product_id, + sku: item.variant.sku, quantity: item.quantity, } }) diff --git a/packages/medusa-plugin-segment/src/subscribers/order.js b/packages/medusa-plugin-segment/src/subscribers/order.js index 4253a3fb62..0c2d08ad65 100644 --- a/packages/medusa-plugin-segment/src/subscribers/order.js +++ b/packages/medusa-plugin-segment/src/subscribers/order.js @@ -1,13 +1,55 @@ class OrderSubscriber { - constructor({ segmentService, eventBusService }) { + constructor({ + segmentService, + eventBusService, + orderService, + returnService, + }) { + this.orderService_ = orderService + + this.returnService_ = returnService + eventBusService.subscribe( "order.items_returned", - async ({ order, return: ret }) => { + async ({ id, return_id }) => { + const order = await this.orderService_.retrieve(id, { + select: [ + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + ], + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) + + const ret = await this.returnService_.retrieve(return_id) + const shipping = [] if (ret.shipping_method && ret.shipping_method.price) { shipping.push({ ...ret.shipping_method, - price: -1 * ret.shipping_method.price, + price: -1 * (ret.shipping_method.price / 100), }) } @@ -15,7 +57,7 @@ class OrderSubscriber { ...order, shipping_methods: shipping, items: ret.items.map((i) => - order.items.find((l) => l._id === i.item_id) + order.items.find((l) => l.id === i.item_id) ), } @@ -31,7 +73,19 @@ class OrderSubscriber { } ) - eventBusService.subscribe("order.canceled", async (order) => { + eventBusService.subscribe("order.canceled", async ({ id }) => { + const order = await this.orderService_.retrieve(id, { + select: [ + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + ], + }) + const date = new Date() const orderData = await segmentService.buildOrder(order) const orderEvent = { @@ -44,7 +98,38 @@ class OrderSubscriber { segmentService.track(orderEvent) }) - eventBusService.subscribe("order.placed", async (order) => { + eventBusService.subscribe("order.placed", async ({ id }) => { + const order = await this.orderService_.retrieve(id, { + select: [ + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + ], + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) + const date = new Date(parseInt(order.created)) const orderData = await segmentService.buildOrder(order) const orderEvent = { diff --git a/packages/medusa-plugin-sendgrid/.npmignore b/packages/medusa-plugin-sendgrid/.npmignore index 486581be18..73122644c5 100644 --- a/packages/medusa-plugin-sendgrid/.npmignore +++ b/packages/medusa-plugin-sendgrid/.npmignore @@ -5,5 +5,9 @@ node_modules /*.js !index.js yarn.lock - +src +.gitignore +.eslintrc +.babelrc +.prettierrc diff --git a/packages/medusa-plugin-sendgrid/package.json b/packages/medusa-plugin-sendgrid/package.json index 897b1aee66..ba7bdd1622 100644 --- a/packages/medusa-plugin-sendgrid/package.json +++ b/packages/medusa-plugin-sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-sendgrid", - "version": "1.0.14", + "version": "1.0.13-alpha.176+0646bd3", "description": "SendGrid transactional emails", "main": "index.js", "repository": { @@ -39,8 +39,8 @@ "@sendgrid/mail": "^7.1.1", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index a0857a1e04..2516b3cd46 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -37,8 +37,7 @@ class SendGridService extends BaseService { templateId = this.options_.gift_card_created_template data = { ...data, - display_value: - data.giftcard.discount_rule.value * (1 + data.tax_rate), + display_value: data.giftcard.rule.value * (1 + data.tax_rate), } break case "order.placed": diff --git a/packages/medusa-plugin-sendgrid/src/subscribers/order.js b/packages/medusa-plugin-sendgrid/src/subscribers/order.js index dc10ec0cad..d58b4c4494 100644 --- a/packages/medusa-plugin-sendgrid/src/subscribers/order.js +++ b/packages/medusa-plugin-sendgrid/src/subscribers/order.js @@ -4,16 +4,52 @@ class OrderSubscriber { orderService, sendgridService, eventBusService, + fulfillmentService, }) { this.orderService_ = orderService this.totalsService_ = totalsService this.sendgridService_ = sendgridService this.eventBus_ = eventBusService + this.fulfillmentService_ = fulfillmentService this.eventBus_.subscribe( "order.shipment_created", - async ({ order_id, shipment }) => { - const order = await this.orderService_.retrieve(order_id) + async ({ id, fulfillment_id }) => { + const order = await this.orderService_.retrieve(id, { + select: [ + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + "refundable_amount", + ], + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "shipping_methods.shipping_option", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) + + const shipment = await this.fulfillmentService_.retrieve(fulfillment_id) + const data = { ...order, tracking_number: shipment.tracking_numbers.join(", "), @@ -33,50 +69,108 @@ class OrderSubscriber { ) }) - this.eventBus_.subscribe("order.placed", async (order) => { - const subtotal = await this.totalsService_.getSubtotal(order) - const tax_total = await this.totalsService_.getTaxTotal(order) - const discount_total = await this.totalsService_.getDiscountTotal(order) - const shipping_total = await this.totalsService_.getShippingTotal(order) - const total = await this.totalsService_.getTotal(order) + this.eventBus_.subscribe("order.placed", async (orderObj) => { + try { + const order = await this.orderService_.retrieve(orderObj.id, { + select: [ + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + ], + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "shipping_methods.shipping_option", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) - const date = new Date(parseInt(order.created)) - const data = { - ...order, - date: date.toDateString(), - items: order.items.map((i) => { + const { + subtotal, + tax_total, + discount_total, + shipping_total, + total, + } = order + + const taxRate = order.tax_rate / 100 + const currencyCode = order.currency_code.toUpperCase() + + const items = order.items.map((i) => { return { ...i, - price: `${(i.content.unit_price * (1 + order.tax_rate)).toFixed( + price: `${((i.unit_price / 100) * (1 + taxRate)).toFixed( 2 - )} ${order.currency_code}`, + )} ${currencyCode}`, } - }), - discounts: order.discounts.map((discount) => { - return { - is_giftcard: discount.is_giftcard, - code: discount.code, - descriptor: `${discount.discount_rule.value}${ - discount.discount_rule.type === "percentage" - ? "%" - : ` ${order.currency_code}` - }`, - } - }), - subtotal: `${(subtotal * (1 + order.tax_rate)).toFixed(2)} ${ - order.currency_code - }`, - tax_total: `${tax_total.toFixed(2)} ${order.currency_code}`, - discount_total: `${(discount_total * (1 + order.tax_rate)).toFixed( - 2 - )} ${order.currency_code}`, - shipping_total: `${(shipping_total * (1 + order.tax_rate)).toFixed( - 2 - )} ${order.currency_code}`, - total: `${total.toFixed(2)} ${order.currency_code}`, - } + }) - await this.sendgridService_.transactionalEmail("order.placed", data) + let discounts = [] + if (order.discounts) { + discounts = order.discounts.map((discount) => { + return { + is_giftcard: false, + code: discount.code, + descriptor: `${discount.rule.value}${ + discount.rule.type === "percentage" ? "%" : ` ${currencyCode}` + }`, + } + }) + } + + let giftCards = [] + if (order.gift_cards) { + giftCards = order.gift_cards.map((gc) => { + return { + is_giftcard: true, + code: gc.code, + descriptor: `${gc.value} ${currencyCode}`, + } + }) + + discounts.concat(giftCards) + } + + const data = { + ...order, + date: order.created_at.toDateString(), + items, + discounts, + subtotal: `${((subtotal / 100) * (1 + taxRate)).toFixed( + 2 + )} ${currencyCode}`, + tax_total: `${(tax_total / 100).toFixed(2)} ${currencyCode}`, + discount_total: `${((discount_total / 100) * (1 + taxRate)).toFixed( + 2 + )} ${currencyCode}`, + shipping_total: `${((shipping_total / 100) * (1 + taxRate)).toFixed( + 2 + )} ${currencyCode}`, + total: `${(total / 100).toFixed(2)} ${currencyCode}`, + } + + await this.sendgridService_.transactionalEmail("order.placed", data) + } catch (error) { + console.log(error) + } }) this.eventBus_.subscribe("order.cancelled", async (order) => { diff --git a/packages/medusa-plugin-slack-notification/.npmignore b/packages/medusa-plugin-slack-notification/.npmignore index 486581be18..604f4c63f1 100644 --- a/packages/medusa-plugin-slack-notification/.npmignore +++ b/packages/medusa-plugin-slack-notification/.npmignore @@ -5,5 +5,7 @@ node_modules /*.js !index.js yarn.lock +src + diff --git a/packages/medusa-plugin-slack-notification/package.json b/packages/medusa-plugin-slack-notification/package.json index 2554a1dc9a..4f9ad0c0ea 100644 --- a/packages/medusa-plugin-slack-notification/package.json +++ b/packages/medusa-plugin-slack-notification/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-slack-notification", - "version": "1.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "Slack notifications", "main": "index.js", "repository": { @@ -39,9 +39,9 @@ "axios": "^0.19.2", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "moment": "^2.27.0" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-slack-notification/src/services/slack.js b/packages/medusa-plugin-slack-notification/src/services/slack.js index b60abc8f40..d67069d9c3 100644 --- a/packages/medusa-plugin-slack-notification/src/services/slack.js +++ b/packages/medusa-plugin-slack-notification/src/services/slack.js @@ -1,5 +1,4 @@ import axios from "axios" -import moment from "moment" import { BaseService } from "medusa-interfaces" class SlackService extends BaseService { @@ -23,20 +22,48 @@ class SlackService extends BaseService { } async orderNotification(orderId) { - const order = await this.orderService_.retrieve(orderId) + const order = await this.orderService_.retrieve(orderId, { + select: [ + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + ], + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) - const subtotal = await this.totalsService_.getSubtotal(order) - const shippingTotal = await this.totalsService_.getShippingTotal(order) - const taxTotal = await this.totalsService_.getTaxTotal(order) - const discountTotal = await this.totalsService_.getDiscountTotal(order) - const total = await this.totalsService_.getTotal(order) + const { subtotal, tax_total, discount_total, shipping_total, total } = order + + const currencyCode = order.currency_code.toUpperCase() + const taxRate = order.tax_rate / 100 let blocks = [ { type: "section", text: { type: "mrkdwn", - text: `Order *<${this.options_.admin_orders_url}/${order._id}|#${order.display_id}>* has been processed.`, + text: `Order *<${this.options_.admin_orders_url}/${order.id}|#${order.display_id}>* has been processed.`, }, }, { @@ -56,15 +83,17 @@ class SlackService extends BaseService { type: "section", text: { type: "mrkdwn", - text: `*Subtotal*\t${subtotal.toFixed(2)} ${ - order.currency_code - }\n*Shipping*\t${shippingTotal.toFixed(2)} ${ - order.currency_code - }\n*Discount Total*\t${discountTotal.toFixed(2)} ${ - order.currency_code - }\n*Tax*\t${taxTotal.toFixed(2)} ${ - order.currency_code - }\n*Total*\t${total.toFixed(2)} ${order.currency_code}`, + text: `*Subtotal*\t${(subtotal / 100).toFixed( + 2 + )} ${currencyCode}\n*Shipping*\t${(shipping_total / 100).toFixed( + 2 + )} ${currencyCode}\n*Discount Total*\t${( + discount_total / 100 + ).toFixed(2)} ${currencyCode}\n*Tax*\t${(tax_total / 100).toFixed( + 2 + )} ${currencyCode}\n*Total*\t${(total / 100).toFixed( + 2 + )} ${currencyCode}`, }, }, ] @@ -74,7 +103,9 @@ class SlackService extends BaseService { type: "section", text: { type: "mrkdwn", - text: `*Promo Code*\t${d.code} ${d.discount_rule.value}${d.discount_rule.type === "percentage" ? "%" : ` ${order.currency_code}`}`, + text: `*Promo Code*\t${d.code} ${d.rule.value}${ + d.rule.type === "percentage" ? "%" : ` ${currencyCode}` + }`, }, }) }) @@ -88,12 +119,13 @@ class SlackService extends BaseService { type: "section", text: { type: "mrkdwn", - text: `*${lineItem.title}*\n${lineItem.quantity} x ${ - !Array.isArray(lineItem.content) && - (lineItem.content.unit_price * (1 + order.tax_rate)).toFixed(2) - } ${order.currency_code}`, + text: `*${lineItem.title}*\n${lineItem.quantity} x ${( + (lineItem.unit_price / 100) * + (1 + taxRate) + ).toFixed(2)} ${currencyCode}`, }, } + if (lineItem.thumbnail) { let url = lineItem.thumbnail if ( @@ -106,9 +138,8 @@ class SlackService extends BaseService { line.accessory = { type: "image", alt_text: "Item", - image_url: url + image_url: url, } - } blocks.push(line) diff --git a/packages/medusa-plugin-slack-notification/src/subscribers/order.js b/packages/medusa-plugin-slack-notification/src/subscribers/order.js index bfa02ddd63..b45fbaa4b3 100644 --- a/packages/medusa-plugin-slack-notification/src/subscribers/order.js +++ b/packages/medusa-plugin-slack-notification/src/subscribers/order.js @@ -4,8 +4,8 @@ class OrderSubscriber { this.eventBus_ = eventBusService - this.eventBus_.subscribe("order.placed", async (order) => { - await this.slackService_.orderNotification(order._id) + this.eventBus_.subscribe("order.placed", async ({ id }) => { + await this.slackService_.orderNotification(id) }) } } diff --git a/packages/medusa-plugin-slack-notification/src/utils/eu-countries.js b/packages/medusa-plugin-slack-notification/src/utils/eu-countries.js deleted file mode 100644 index 29bfa4d625..0000000000 --- a/packages/medusa-plugin-slack-notification/src/utils/eu-countries.js +++ /dev/null @@ -1,30 +0,0 @@ -export default [ - "BE", - "BG", - "CZ", - "DK", - "DE", - "EE", - "IE", - "EL", - "ES", - "FR", - "HR", - "IT", - "CY", - "LV", - "LT", - "LU", - "HU", - "MT", - "NL", - "AT", - "PL", - "PT", - "RO", - "SI", - "SK", - "FI", - "SE", - "UK", -] diff --git a/packages/medusa-plugin-slack-notification/utils/eu-countries.js b/packages/medusa-plugin-slack-notification/utils/eu-countries.js deleted file mode 100644 index 7b89c327ef..0000000000 --- a/packages/medusa-plugin-slack-notification/utils/eu-countries.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports["default"] = void 0; -var _default = ["BE", "BG", "CZ", "DK", "DE", "EE", "IE", "EL", "ES", "FR", "HR", "IT", "CY", "LV", "LT", "LU", "HU", "MT", "NL", "AT", "PL", "PT", "RO", "SI", "SK", "FI", "SE", "UK"]; -exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-twilio-sms/package.json b/packages/medusa-plugin-twilio-sms/package.json index ab062254cc..966f2d339e 100644 --- a/packages/medusa-plugin-twilio-sms/package.json +++ b/packages/medusa-plugin-twilio-sms/package.json @@ -1,7 +1,6 @@ { "name": "medusa-plugin-twilio-sms", - "version": "1.0.12", - "description": "Twilio SMS service", + "version": "1.0.11-alpha.176+0646bd3", "main": "index.js", "repository": { "type": "git", @@ -36,9 +35,9 @@ "dependencies": { "@babel/plugin-transform-classes": "^7.9.5", "body-parser": "^1.19.0", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "twilio": "^3.49.1" }, - "gitHead": "e1c465d244b20e68dd88bcb12db0a061f25581ec" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-plugin-wishlist/package.json b/packages/medusa-plugin-wishlist/package.json index f41434c844..7fcb215950 100644 --- a/packages/medusa-plugin-wishlist/package.json +++ b/packages/medusa-plugin-wishlist/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-wishlist", - "version": "1.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "Provides /customers/:id/wishlist to add items to a customr's wishlist", "main": "index.js", "repository": { @@ -37,8 +37,8 @@ "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-test-utils/package.json b/packages/medusa-test-utils/package.json index 6231316d4b..872f7f3bab 100644 --- a/packages/medusa-test-utils/package.json +++ b/packages/medusa-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "medusa-test-utils", - "version": "1.0.13", + "version": "1.0.12-alpha.176+0646bd3", "description": "Test utils for Medusa", "main": "dist/index.js", "repository": { @@ -29,8 +29,8 @@ }, "dependencies": { "@babel/plugin-transform-classes": "^7.9.5", - "medusa-core-utils": "^1.0.11", - "mongoose": "^5.8.0" + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "randomatic": "^3.1.1" }, - "gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" + "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" } diff --git a/packages/medusa-test-utils/src/id-map.js b/packages/medusa-test-utils/src/id-map.js index ffc435ce24..c3af2c5520 100644 --- a/packages/medusa-test-utils/src/id-map.js +++ b/packages/medusa-test-utils/src/id-map.js @@ -1,23 +1,19 @@ -import mongoose from "mongoose" - -String.prototype.equals = function(that) { - return this === that -} +import randomize from "randomatic"; class IdMap { - ids = {} + ids = {}; - getId(key, backend=false) { + getId(key, prefix = "", length = 10) { if (this.ids[key]) { - return this.ids[key] + return this.ids[key]; } - const mongooseId = `${mongoose.Types.ObjectId()}` - this.ids[key] = mongooseId + const id = `${prefix && prefix + "_"}${randomize("Aa0", length)}`; + this.ids[key] = id; - return mongooseId + return id; } } -const instance = new IdMap() -export default instance +const instance = new IdMap(); +export default instance; diff --git a/packages/medusa-test-utils/src/index.js b/packages/medusa-test-utils/src/index.js index 1abf0390d2..777ef14117 100644 --- a/packages/medusa-test-utils/src/index.js +++ b/packages/medusa-test-utils/src/index.js @@ -1 +1,3 @@ -export { default as IdMap } from "./id-map" +export { default as IdMap } from "./id-map"; +export { default as MockRepository } from "./mock-repository"; +export { default as MockManager } from "./mock-manager"; diff --git a/packages/medusa-test-utils/src/mock-manager.js b/packages/medusa-test-utils/src/mock-manager.js new file mode 100644 index 0000000000..1205c4693e --- /dev/null +++ b/packages/medusa-test-utils/src/mock-manager.js @@ -0,0 +1,13 @@ +export default { + getCustomRepository: function (repo) { + return repo; + }, + + transaction: function (isolationOrCb, cb) { + if (typeof isolationOrCb === "string") { + return cb(this); + } else { + return isolationOrCb(this); + } + }, +}; diff --git a/packages/medusa-test-utils/src/mock-repository.js b/packages/medusa-test-utils/src/mock-repository.js new file mode 100644 index 0000000000..adde45066d --- /dev/null +++ b/packages/medusa-test-utils/src/mock-repository.js @@ -0,0 +1,67 @@ +export default ({ + create, + update, + remove, + softRemove, + find, + findOne, + findOneOrFail, + save, +} = {}) => { + return { + create: jest.fn().mockImplementation((...args) => { + if (create) { + return create(...args); + } + return {}; + }), + softRemove: jest.fn().mockImplementation((...args) => { + if (softRemove) { + return softRemove(...args); + } + return {}; + }), + remove: jest.fn().mockImplementation((...args) => { + if (remove) { + return remove(...args); + } + return {}; + }), + update: jest.fn().mockImplementation((...args) => { + if (update) { + return update(...args); + } + }), + findOneOrFail: jest.fn().mockImplementation((...args) => { + if (findOneOrFail) { + return findOneOrFail(...args); + } + }), + findOne: jest.fn().mockImplementation((...args) => { + if (findOne) { + return findOne(...args); + } + }), + findOneOrFail: jest.fn().mockImplementation((...args) => { + if (findOneOrFail) { + return findOneOrFail(...args); + } + }), + find: jest.fn().mockImplementation((...args) => { + if (find) { + return find(...args); + } + }), + softRemove: jest.fn().mockImplementation((...args) => { + if (softRemove) { + return softRemove(...args); + } + }), + save: jest.fn().mockImplementation((...args) => { + if (save) { + return save(...args); + } + return Promise.resolve(...args); + }), + }; +}; diff --git a/packages/medusa-test-utils/yarn.lock b/packages/medusa-test-utils/yarn.lock index 4a54213137..69c8687d23 100644 --- a/packages/medusa-test-utils/yarn.lock +++ b/packages/medusa-test-utils/yarn.lock @@ -1540,11 +1540,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1604,11 +1599,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@^1.1.1, bson@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" - integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -1882,13 +1872,6 @@ data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2777,6 +2760,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -3363,11 +3351,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -kareem@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" - integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== - kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -3487,10 +3470,17 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +<<<<<<< HEAD +math-random@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== +======= memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== +>>>>>>> master merge-stream@^2.0.0: version "2.0.0" @@ -3578,61 +3568,12 @@ mkdirp@^0.5.1: dependencies: minimist "0.0.8" -mongodb@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.4.1.tgz#0d15e57e0ea0fc85b7a4fb9291b374c2e71652dc" - integrity sha512-juqt5/Z42J4DcE7tG7UdVaTKmUC6zinF4yioPfpeOSNBieWSK6qCY+0tfGQcHLKrauWPDdMZVROHJOa8q2pWsA== - dependencies: - bson "^1.1.1" - require_optional "^1.0.1" - safe-buffer "^5.1.2" - optionalDependencies: - saslprep "^1.0.0" - -mongoose-legacy-pluralize@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" - integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== - -mongoose@^5.8.0: - version "5.8.10" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.8.10.tgz#c06c6a7e171c8b0706bf85da7f98e478fa6f9822" - integrity sha512-3sRiZhtMIB4egqxWbry23C+xX87kQ0aTvPtMXxWXCBGfCRkXMJl/CLiftYcle/JPy09Lv5u+ZCBpIJUgwDMtxw== - dependencies: - bson "~1.1.1" - kareem "2.3.1" - mongodb "3.4.1" - mongoose-legacy-pluralize "1.0.2" - mpath "0.6.0" - mquery "3.2.2" - ms "2.1.2" - regexp-clone "1.0.0" - safe-buffer "5.1.2" - sift "7.0.1" - sliced "1.0.1" - -mpath@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" - integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== - -mquery@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" - integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== - dependencies: - bluebird "3.5.1" - debug "3.1.0" - regexp-clone "^1.0.0" - safe-buffer "5.1.2" - sliced "1.0.1" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.2, ms@^2.1.1: +ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -4008,6 +3949,15 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +randomatic@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" + integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + react-is@^16.12.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -4091,11 +4041,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-clone@1.0.0, regexp-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" - integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== - regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -4192,14 +4137,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -require_optional@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" - integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== - dependencies: - resolve-from "^2.0.0" - semver "^5.1.0" - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -4207,11 +4144,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-from@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" - integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4292,16 +4224,16 @@ rxjs@^6.5.3: dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -4329,13 +4261,6 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -saslprep@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" - integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== - dependencies: - sparse-bitfield "^3.0.3" - saxes@^3.1.9: version "3.1.11" resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" @@ -4343,7 +4268,7 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4402,11 +4327,6 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -sift@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" - integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== - signal-exit@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4441,11 +4361,6 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -sliced@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" - integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -4515,13 +4430,6 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -sparse-bitfield@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" - integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= - dependencies: - memory-pager "^1.0.2" - spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" diff --git a/packages/medusa/.babelrc.js b/packages/medusa/.babelrc.js index 50ddedf501..ae10f028d4 100644 --- a/packages/medusa/.babelrc.js +++ b/packages/medusa/.babelrc.js @@ -7,6 +7,6 @@ if (process.env.NODE_ENV !== `test`) { } module.exports = { - presets: [["babel-preset-medusa-package"]], + presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]], ignore, } diff --git a/packages/medusa/ormconfig.json b/packages/medusa/ormconfig.json new file mode 100644 index 0000000000..2eb859eefb --- /dev/null +++ b/packages/medusa/ormconfig.json @@ -0,0 +1,5 @@ +{ + "cli": { + "migrationsDir": "src/migrations" + } +} diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 20afa8d39f..043802f54c 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa", - "version": "1.0.56", + "version": "1.0.33-alpha.205+8c87f25", "description": "E-commerce for JAMstack", "main": "dist/app.js", "repository": { @@ -18,12 +18,14 @@ "@babel/core": "^7.7.5", "@babel/node": "^7.7.4", "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-proposal-decorators": "^7.12.1", "@babel/plugin-transform-instanceof": "^7.8.3", "@babel/plugin-transform-runtime": "^7.7.6", "@babel/preset-env": "^7.7.5", + "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.7.4", "@babel/runtime": "^7.7.6", - "babel-preset-medusa-package": "^1.0.1", + "babel-preset-medusa-package": "^1.0.2-alpha.787+0646bd3", "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.2", @@ -33,9 +35,9 @@ }, "scripts": { "start": "nodemon --watch plugins/ --watch src/ --exec babel-node src/app.js", - "watch": "babel -w src --out-dir dist/ --ignore **/__tests__", + "watch": "babel -w src --out-dir dist/ --ignore **/__tests__ --extensions \".ts,.js\"", "prepare": "cross-env NODE_ENV=production npm run build", - "build": "babel src -d dist --ignore **/__tests__", + "build": "babel src -d dist --ignore **/__tests__ --extensions \".ts,.js\"", "serve": "node dist/app.js", "test": "jest" }, @@ -60,21 +62,28 @@ "fs-exists-cached": "^1.0.0", "glob": "^7.1.6", "ioredis": "^4.17.3", + "joi": "^17.3.0", "joi-objectid": "^3.0.1", "jsonwebtoken": "^8.5.1", - "medusa-core-utils": "^1.0.11", - "medusa-test-utils": "^1.0.13", + "medusa-core-utils": "^1.0.12-alpha.787+0646bd3", + "medusa-test-utils": "^1.0.12-alpha.176+0646bd3", "morgan": "^1.9.1", "multer": "^1.4.2", "passport": "^0.4.0", "passport-http-bearer": "^1.0.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "pg": "^8.5.1", "randomatic": "^3.1.1", "redis": "^3.0.2", + "reflect-metadata": "^0.1.13", + "request-ip": "^2.1.3", "resolve-cwd": "^3.0.0", "scrypt-kdf": "^2.0.1", + "typeorm": "^0.2.29", + "ulid": "^2.3.0", + "uuid": "^8.3.1", "winston": "^3.2.1" }, - "gitHead": "b54e28769a423c9285a1119535cfa1590d08a559" + "gitHead": "8c87f25f766154368b0f15424028550966f7d3d6" } diff --git a/packages/medusa/src/api/middlewares/error-handler.js b/packages/medusa/src/api/middlewares/error-handler.js index fe330646cb..6ea715bfd0 100644 --- a/packages/medusa/src/api/middlewares/error-handler.js +++ b/packages/medusa/src/api/middlewares/error-handler.js @@ -5,6 +5,8 @@ export default () => { const logger = req.scope.resolve("logger") logger.error(err.message) + console.log(err) + let statusCode = 500 switch (err.name) { case MedusaError.Types.NOT_ALLOWED: diff --git a/packages/medusa/src/api/routes/admin/auth/create-session.js b/packages/medusa/src/api/routes/admin/auth/create-session.js index 5114eba3a9..dbcdb4fa3e 100644 --- a/packages/medusa/src/api/routes/admin/auth/create-session.js +++ b/packages/medusa/src/api/routes/admin/auth/create-session.js @@ -22,7 +22,7 @@ export default async (req, res) => { } // Add JWT to cookie - req.session.jwt = jwt.sign({ userId: result.user._id }, config.jwtSecret, { + req.session.jwt = jwt.sign({ userId: result.user.id }, config.jwtSecret, { expiresIn: "24h", }) diff --git a/packages/medusa/src/api/routes/admin/customers/create-customer.js b/packages/medusa/src/api/routes/admin/customers/create-customer.js index 094267f07d..32d4cef0c0 100644 --- a/packages/medusa/src/api/routes/admin/customers/create-customer.js +++ b/packages/medusa/src/api/routes/admin/customers/create-customer.js @@ -18,8 +18,7 @@ export default async (req, res) => { try { const customerService = req.scope.resolve("customerService") const customer = await customerService.create(value) - const data = await customerService.decorate(customer, ["_id", "email"]) - res.status(201).json({ customer: data }) + res.status(201).json({ customer }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/customers/get-customer.js b/packages/medusa/src/api/routes/admin/customers/get-customer.js index 30924754ab..65841fb91b 100644 --- a/packages/medusa/src/api/routes/admin/customers/get-customer.js +++ b/packages/medusa/src/api/routes/admin/customers/get-customer.js @@ -1,28 +1,10 @@ export default async (req, res) => { const { id } = req.params try { - const orderService = req.scope.resolve("orderService") const customerService = req.scope.resolve("customerService") - let customer = await customerService.retrieve(id) - customer = await customerService.decorate( - customer, - [ - "email", - "payment_methods", - "has_account", - "shipping_addresses", - "phone", - ], - [] - ) - - customer.orders = await orderService.list({ customer_id: customer._id }) - - customer.orders = await Promise.all( - customer.orders.map(order => { - return orderService.decorate(order, ["total", "payment_status"], []) - }) - ) + const customer = await customerService.retrieve(id, { + relations: ["orders"], + }) res.json({ customer }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/customers/list-customers.js b/packages/medusa/src/api/routes/admin/customers/list-customers.js index f1aa3fcebd..6683f80e06 100644 --- a/packages/medusa/src/api/routes/admin/customers/list-customers.js +++ b/packages/medusa/src/api/routes/admin/customers/list-customers.js @@ -1,22 +1,19 @@ export default async (req, res) => { try { const customerService = req.scope.resolve("customerService") - const queryBuilderService = req.scope.resolve("queryBuilderService") - const query = queryBuilderService.buildQuery(req.query, [ - "email", - "first_name", - "last_name", - ]) - - const limit = parseInt(req.query.limit) || 0 + const limit = parseInt(req.query.limit) || 10 const offset = parseInt(req.query.offset) || 0 - const customers = await customerService.list(query, offset, limit) + const listConfig = { + relations: [], + skip: offset, + take: limit, + } - const numCustomers = await customerService.count() + const customers = await customerService.list({}, listConfig) - res.json({ customers, total_count: numCustomers }) + res.json({ customers, count: customers.length, offset, limit }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/customers/update-customer.js b/packages/medusa/src/api/routes/admin/customers/update-customer.js index e602de9518..4d63e92400 100644 --- a/packages/medusa/src/api/routes/admin/customers/update-customer.js +++ b/packages/medusa/src/api/routes/admin/customers/update-customer.js @@ -20,15 +20,10 @@ export default async (req, res) => { const customerService = req.scope.resolve("customerService") await customerService.update(id, value) - const customer = await customerService.retrieve(id) - const data = await customerService.decorate(customer) - data.orders = await Promise.all( - customer.orders.map(async oId => { - const order = await orderService.retrieve(oId) - return orderService.decorate(order, [], []) - }) - ) - res.status(200).json({ customer: data }) + const customer = await customerService.retrieve(id, { + relations: ["orders"], + }) + res.status(200).json({ customer }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js index 2a5c876568..32637d49ce 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js @@ -29,7 +29,23 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => { it("calls service retrieve", () => { expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("total10") + IdMap.getId("total10"), + { + select: [ + "id", + "code", + "is_dynamic", + "rule_id", + "parent_discount_id", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", + ], + relations: ["rule", "parent_discount", "regions", "rule.valid_for"], + } ) expect(DiscountServiceMock.addRegion).toHaveBeenCalledTimes(1) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-variant.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js similarity index 60% rename from packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-variant.js rename to packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js index d4c33d647f..63e43fb241 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-variant.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js @@ -9,7 +9,7 @@ describe("POST /admin/discounts/:discount_id/variants/:variant_id", () => { beforeAll(async () => { subject = await request( "POST", - `/admin/discounts/${IdMap.getId("total10")}/variants/${IdMap.getId( + `/admin/discounts/${IdMap.getId("total10")}/products/${IdMap.getId( "testVariant" )}`, { @@ -29,11 +29,27 @@ describe("POST /admin/discounts/:discount_id/variants/:variant_id", () => { it("calls service retrieve", () => { expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("total10") + IdMap.getId("total10"), + { + select: [ + "id", + "code", + "is_dynamic", + "rule_id", + "parent_discount_id", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", + ], + relations: ["rule", "parent_discount", "regions", "rule.valid_for"], + } ) - expect(DiscountServiceMock.addValidVariant).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.addValidVariant).toHaveBeenCalledWith( + expect(DiscountServiceMock.addValidProduct).toHaveBeenCalledTimes(1) + expect(DiscountServiceMock.addValidProduct).toHaveBeenCalledWith( IdMap.getId("total10"), IdMap.getId("testVariant") ) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js index c39611074a..cde96a2332 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js @@ -10,7 +10,7 @@ describe("POST /admin/discounts", () => { subject = await request("POST", "/admin/discounts", { payload: { code: "TEST", - discount_rule: { + rule: { type: "fixed", value: 10, allocation: "total", @@ -32,12 +32,13 @@ describe("POST /admin/discounts", () => { expect(DiscountServiceMock.create).toHaveBeenCalledTimes(1) expect(DiscountServiceMock.create).toHaveBeenCalledWith({ code: "TEST", - discount_rule: { + rule: { type: "fixed", value: 10, allocation: "total", }, - is_dynamic: false + is_disabled: false, + is_dynamic: false, }) }) }) @@ -49,7 +50,7 @@ describe("POST /admin/discounts", () => { subject = await request("POST", "/admin/discounts", { payload: { code: "10%OFF", - discount_rule: { + rule: { value: 10, allocation: "total", }, @@ -67,9 +68,7 @@ describe("POST /admin/discounts", () => { }) it("returns error", () => { - expect(subject.body.message[0].message).toEqual( - `"discount_rule.type" is required` - ) + expect(subject.body.message[0].message).toEqual(`"rule.type" is required`) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js index 304799289b..a9b2e86070 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js @@ -2,6 +2,27 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { DiscountServiceMock } from "../../../../../services/__mocks__/discount" +const defaultFields = [ + "id", + "code", + "is_dynamic", + "rule_id", + "parent_discount_id", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = [ + "rule", + "parent_discount", + "regions", + "rule.valid_for", +] + describe("GET /admin/discounts/:discount_id", () => { describe("successful retrieval", () => { let subject @@ -27,7 +48,11 @@ describe("GET /admin/discounts/:discount_id", () => { it("calls service retrieve", () => { expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("total10") + IdMap.getId("total10"), + { + select: defaultFields, + relations: defaultRelations, + } ) }) }) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/list-discounts.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/list-discounts.js index a5342463fb..f98398511f 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/list-discounts.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/list-discounts.js @@ -23,33 +23,6 @@ describe("GET /admin/discounts", () => { it("calls service retrieve", () => { expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.list).toHaveBeenCalledWith({}) - }) - }) - - describe("is_giftcard filter", () => { - let subject - - beforeAll(async () => { - jest.clearAllMocks() - subject = await request("GET", `/admin/discounts?is_giftcard=true`, { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - }) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service retrieve", () => { - expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.list).toHaveBeenCalledWith({ - is_giftcard: true, - }) }) }) @@ -72,12 +45,7 @@ describe("GET /admin/discounts", () => { }) it("calls service retrieve", () => { - expect(DiscountServiceMock.decorate).toHaveBeenCalledTimes(1) - expect( - DiscountServiceMock.decorate - ).toHaveBeenCalledWith(expect.anything(), expect.anything(), ["regions"]) expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.list).toHaveBeenCalledWith({}) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js index 66e862fc51..95ee64c429 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js @@ -2,6 +2,27 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { DiscountServiceMock } from "../../../../../services/__mocks__/discount" +const defaultFields = [ + "id", + "code", + "is_dynamic", + "rule_id", + "parent_discount_id", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = [ + "rule", + "parent_discount", + "regions", + "rule.valid_for", +] + describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => { describe("successful removal", () => { let subject @@ -29,7 +50,11 @@ describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => { it("calls service retrieve", () => { expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("total10") + IdMap.getId("total10"), + { + select: defaultFields, + relations: defaultRelations, + } ) expect(DiscountServiceMock.removeRegion).toHaveBeenCalledTimes(1) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-variant.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js similarity index 58% rename from packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-variant.js rename to packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js index 0882c76758..d8fb2636d2 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-variant.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js @@ -2,14 +2,35 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { DiscountServiceMock } from "../../../../../services/__mocks__/discount" -describe("DELETE /admin/discounts/:discount_id/variants/:variant_id", () => { +const defaultFields = [ + "id", + "code", + "is_dynamic", + "rule_id", + "parent_discount_id", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = [ + "rule", + "parent_discount", + "regions", + "rule.valid_for", +] + +describe("DELETE /admin/discounts/:discount_id/products/:variant_id", () => { describe("successful addition", () => { let subject beforeAll(async () => { subject = await request( "DELETE", - `/admin/discounts/${IdMap.getId("total10")}/variants/${IdMap.getId( + `/admin/discounts/${IdMap.getId("total10")}/products/${IdMap.getId( "testVariant" )}`, { @@ -29,11 +50,15 @@ describe("DELETE /admin/discounts/:discount_id/variants/:variant_id", () => { it("calls service retrieve", () => { expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("total10") + IdMap.getId("total10"), + { + select: defaultFields, + relations: defaultRelations, + } ) - expect(DiscountServiceMock.removeValidVariant).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.removeValidVariant).toHaveBeenCalledWith( + expect(DiscountServiceMock.removeValidProduct).toHaveBeenCalledTimes(1) + expect(DiscountServiceMock.removeValidProduct).toHaveBeenCalledWith( IdMap.getId("total10"), IdMap.getId("testVariant") ) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/update-discount.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/update-discount.js index cea4201934..fd7ab4b7b5 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/update-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/update-discount.js @@ -13,7 +13,8 @@ describe("POST /admin/discounts", () => { { payload: { code: "10TOTALOFF", - discount_rule: { + rule: { + id: "1234", type: "fixed", value: 10, allocation: "total", @@ -38,7 +39,8 @@ describe("POST /admin/discounts", () => { IdMap.getId("total10"), { code: "10TOTALOFF", - discount_rule: { + rule: { + id: "1234", type: "fixed", value: 10, allocation: "total", diff --git a/packages/medusa/src/api/routes/admin/discounts/add-region.js b/packages/medusa/src/api/routes/admin/discounts/add-region.js index 2140449eef..75128ab73a 100644 --- a/packages/medusa/src/api/routes/admin/discounts/add-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/add-region.js @@ -1,4 +1,4 @@ -import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { discount_id, region_id } = req.params @@ -7,8 +7,12 @@ export default async (req, res) => { await discountService.addRegion(discount_id, region_id) - const data = discountService.retrieve(discount_id) - res.status(200).json({ discount: data }) + const discount = await discountService.retrieve(discount_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ discount }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/add-valid-product.js b/packages/medusa/src/api/routes/admin/discounts/add-valid-product.js new file mode 100644 index 0000000000..86072e09d4 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/discounts/add-valid-product.js @@ -0,0 +1,20 @@ +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const { discount_id, variant_id } = req.params + + try { + const discountService = req.scope.resolve("discountService") + + await discountService.addValidProduct(discount_id, variant_id) + + const discount = await discountService.retrieve(discount_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ discount }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/discounts/add-valid-variant.js b/packages/medusa/src/api/routes/admin/discounts/add-valid-variant.js deleted file mode 100644 index 1cd829f4f8..0000000000 --- a/packages/medusa/src/api/routes/admin/discounts/add-valid-variant.js +++ /dev/null @@ -1,15 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { discount_id, variant_id } = req.params - try { - const discountService = req.scope.resolve("discountService") - - await discountService.addValidVariant(discount_id, variant_id) - - const data = discountService.retrieve(discount_id) - res.status(200).json({ discount: data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/discounts/create-discount.js b/packages/medusa/src/api/routes/admin/discounts/create-discount.js index 0abc872536..c4fa592a15 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/create-discount.js @@ -4,7 +4,7 @@ export default async (req, res) => { const schema = Validator.object().keys({ code: Validator.string().required(), is_dynamic: Validator.boolean().default(false), - discount_rule: Validator.object() + rule: Validator.object() .keys({ description: Validator.string().optional(), type: Validator.string().required(), @@ -14,11 +14,9 @@ export default async (req, res) => { allocation: Validator.string().required(), valid_for: Validator.array().items(Validator.string()), usage_limit: Validator.number().optional(), - total_limit: Validator.number().optional(), }) .required(), - usage_count: Validator.number().optional(), - disabled: Validator.boolean().optional(), + is_disabled: Validator.boolean().default(false), starts_at: Validator.date().optional(), ends_at: Validator.date().optional(), regions: Validator.array() @@ -34,9 +32,15 @@ export default async (req, res) => { try { const discountService = req.scope.resolve("discountService") - const data = await discountService.create(value) - res.status(200).json({ discount: data }) + const created = await discountService.create(value) + const discount = await discountService.retrieve(created.id, [ + "rule", + "rule.valid_for", + "regions", + ]) + + res.status(200).json({ discount }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js b/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js index 9db21e983f..09d08cbea6 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js +++ b/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js @@ -2,6 +2,7 @@ import { MedusaError, Validator } from "medusa-core-utils" export default async (req, res) => { const { discount_id } = req.params + const schema = Validator.object().keys({ code: Validator.string().required(), metadata: Validator.object().optional(), @@ -16,9 +17,11 @@ export default async (req, res) => { const discountService = req.scope.resolve("discountService") await discountService.createDynamicCode(discount_id, value) - const data = await discountService.retrieve(dicsount_id) + const discount = await discountService.retrieve(discount_id, { + relations: ["rule", "rule.valid_for", "regions"], + }) - res.status(200).json({ discount: data }) + res.status(200).json({ discount }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/delete-discount.js b/packages/medusa/src/api/routes/admin/discounts/delete-discount.js index 63d4d487a4..5cecd87bb5 100644 --- a/packages/medusa/src/api/routes/admin/discounts/delete-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/delete-discount.js @@ -1,5 +1,6 @@ export default async (req, res) => { const { discount_id } = req.params + try { const discountService = req.scope.resolve("discountService") await discountService.delete(discount_id) diff --git a/packages/medusa/src/api/routes/admin/discounts/delete-dynamic-code.js b/packages/medusa/src/api/routes/admin/discounts/delete-dynamic-code.js index d9a9b14733..81160abe10 100644 --- a/packages/medusa/src/api/routes/admin/discounts/delete-dynamic-code.js +++ b/packages/medusa/src/api/routes/admin/discounts/delete-dynamic-code.js @@ -1,20 +1,15 @@ -import { MedusaError, Validator } from "medusa-core-utils" - export default async (req, res) => { const { discount_id, code } = req.params - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - try { const discountService = req.scope.resolve("discountService") await discountService.deleteDynamicCode(discount_id, code) - const data = await discountService.retrieve(dicsount_id) + const discount = await discountService.retrieve(discount_id, { + relations: ["rule", "rule.valid_for", "regions"], + }) - res.status(200).json({ discount: data }) + res.status(200).json({ discount }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/get-discount.js b/packages/medusa/src/api/routes/admin/discounts/get-discount.js index 075d8d8c11..3c7dd052dc 100644 --- a/packages/medusa/src/api/routes/admin/discounts/get-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/get-discount.js @@ -1,25 +1,15 @@ +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { const { discount_id } = req.params try { const discountService = req.scope.resolve("discountService") - const data = await discountService.retrieve(discount_id) + const data = await discountService.retrieve(discount_id, { + select: defaultFields, + relations: defaultRelations, + }) - const discount = await discountService.decorate( - data, - [ - "code", - "is_dynamic", - "discount_rule", - "usage_count", - "disabled", - "starts_at", - "ends_at", - "regions", - ], - ["valid_for"] - ) - - res.status(200).json({ discount }) + res.status(200).json({ discount: data }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/index.js b/packages/medusa/src/api/routes/admin/discounts/index.js index d15271ccb2..a38c7c223d 100644 --- a/packages/medusa/src/api/routes/admin/discounts/index.js +++ b/packages/medusa/src/api/routes/admin/discounts/index.js @@ -34,12 +34,12 @@ export default app => { // Discount valid variants management route.post( - "/:discount_id/variants/:variant_id", - middlewares.wrap(require("./add-valid-variant").default) + "/:discount_id/products/:variant_id", + middlewares.wrap(require("./add-valid-product").default) ) route.delete( - "/:discount_id/variants/:variant_id", - middlewares.wrap(require("./remove-valid-variant").default) + "/:discount_id/products/:variant_id", + middlewares.wrap(require("./remove-valid-product").default) ) // Discount region management @@ -54,3 +54,24 @@ export default app => { return app } + +export const defaultFields = [ + "id", + "code", + "is_dynamic", + "rule_id", + "parent_discount_id", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const defaultRelations = [ + "rule", + "parent_discount", + "regions", + "rule.valid_for", +] diff --git a/packages/medusa/src/api/routes/admin/discounts/list-discounts.js b/packages/medusa/src/api/routes/admin/discounts/list-discounts.js index 9ed735f8d6..4e9dee5d0a 100644 --- a/packages/medusa/src/api/routes/admin/discounts/list-discounts.js +++ b/packages/medusa/src/api/routes/admin/discounts/list-discounts.js @@ -1,36 +1,24 @@ +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { try { const selector = {} + const limit = parseInt(req.query.limit) || 20 + const offset = parseInt(req.query.offset) || 0 + const discountService = req.scope.resolve("discountService") - if ("is_giftcard" in req.query) { - selector.is_giftcard = req.query.is_giftcard === "true" + const listConfig = { + select: defaultFields, + relations: defaultRelations, + skip: offset, + take: limit, } - let expandFields = [] - if ("expand_fields" in req.query) { - expandFields = req.query.expand_fields.split(",") - } + const discounts = await discountService.list(selector, listConfig) - let includeFields = [ - "usage_count", - "starts_at", - "ends_at", - "original_amount", - "created", - ] - if ("fields" in req.query) { - includeFields = req.query.fields.split(",") - } - - const raw = await discountService.list(selector) - - const data = await Promise.all( - raw.map(d => discountService.decorate(d, includeFields, expandFields)) - ) - - res.status(200).json({ discounts: data }) + res.status(200).json({ discounts }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/remove-region.js b/packages/medusa/src/api/routes/admin/discounts/remove-region.js index 3eb9c887b6..358533cb0d 100644 --- a/packages/medusa/src/api/routes/admin/discounts/remove-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/remove-region.js @@ -1,3 +1,5 @@ +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { const { discount_id, region_id } = req.params @@ -5,9 +7,12 @@ export default async (req, res) => { const discountService = req.scope.resolve("discountService") await discountService.removeRegion(discount_id, region_id) + const discount = await discountService.retrieve(discount_id, { + select: defaultFields, + relations: defaultRelations, + }) - const data = discountService.retrieve(discount_id) - res.status(200).json({ discounts: data }) + res.status(200).json({ discount }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/discounts/remove-valid-product.js b/packages/medusa/src/api/routes/admin/discounts/remove-valid-product.js new file mode 100644 index 0000000000..ed620dfc47 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/discounts/remove-valid-product.js @@ -0,0 +1,20 @@ +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const { discount_id, variant_id } = req.params + + try { + const discountService = req.scope.resolve("discountService") + + await discountService.removeValidProduct(discount_id, variant_id) + + const discount = await discountService.retrieve(discount_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ discount }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/discounts/remove-valid-variant.js b/packages/medusa/src/api/routes/admin/discounts/remove-valid-variant.js deleted file mode 100644 index 2881eae241..0000000000 --- a/packages/medusa/src/api/routes/admin/discounts/remove-valid-variant.js +++ /dev/null @@ -1,14 +0,0 @@ -export default async (req, res) => { - const { discount_id, variant_id } = req.params - - try { - const discountService = req.scope.resolve("discountService") - - await discountService.removeValidVariant(discount_id, variant_id) - - const data = discountService.retrieve(discount_id) - res.status(200).json({ discounts: data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/discounts/update-discount.js b/packages/medusa/src/api/routes/admin/discounts/update-discount.js index b2941cb376..f8b6f94903 100644 --- a/packages/medusa/src/api/routes/admin/discounts/update-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/update-discount.js @@ -1,23 +1,22 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { discount_id } = req.params const schema = Validator.object().keys({ code: Validator.string().optional(), is_dynamic: Validator.boolean().default(false), - is_giftcard: Validator.boolean().optional(), - discount_rule: Validator.object() + rule: Validator.object() .keys({ + id: Validator.string().required(), description: Validator.string().optional(), type: Validator.string().required(), value: Validator.number().required(), allocation: Validator.string().required(), valid_for: Validator.array().items(Validator.string()), - usage_limit: Validator.number().optional(), }) .optional(), - usage_count: Validator.number().optional(), - disabled: Validator.boolean().optional(), + is_disabled: Validator.boolean().optional(), starts_at: Validator.date().optional(), ends_at: Validator.date().optional(), regions: Validator.array() @@ -35,9 +34,12 @@ export default async (req, res) => { await discountService.update(discount_id, value) - const data = await discountService.retrieve(discount_id) + const discount = await discountService.retrieve(discount_id, { + select: defaultFields, + relations: defaultRelations, + }) - res.status(200).json({ discounts: data }) + res.status(200).json({ discount }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.js b/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.js new file mode 100644 index 0000000000..19734fef72 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.js @@ -0,0 +1,34 @@ +import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const schema = Validator.object().keys({ + value: Validator.number() + .integer() + .optional(), + ends_at: Validator.date().optional(), + is_disabled: Validator.boolean().optional(), + region_id: Validator.string().optional(), + metadata: Validator.object().optional(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const giftCardService = req.scope.resolve("giftCardService") + + await giftCardService.create(value) + + const giftCard = await giftCardService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ gift_card: giftCard }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/gift-cards/delete-gift-card.js b/packages/medusa/src/api/routes/admin/gift-cards/delete-gift-card.js new file mode 100644 index 0000000000..9e789716b3 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/gift-cards/delete-gift-card.js @@ -0,0 +1,16 @@ +export default async (req, res) => { + const { id } = req.params + + try { + const giftCardService = req.scope.resolve("giftCardService") + await giftCardService.delete(id) + + res.json({ + id, + object: "gift-card", + deleted: true, + }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/gift-cards/get-gift-card.js b/packages/medusa/src/api/routes/admin/gift-cards/get-gift-card.js new file mode 100644 index 0000000000..b40021ae48 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/gift-cards/get-gift-card.js @@ -0,0 +1,17 @@ +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const { id } = req.params + + try { + const giftCardService = req.scope.resolve("giftCardService") + const giftCard = await giftCardService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ gift_card: giftCard }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/gift-cards/index.js b/packages/medusa/src/api/routes/admin/gift-cards/index.js new file mode 100644 index 0000000000..ea3a04f5ee --- /dev/null +++ b/packages/medusa/src/api/routes/admin/gift-cards/index.js @@ -0,0 +1,52 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/gift-cards", route) + + route.get("/", middlewares.wrap(require("./list-gift-cards").default)) + + route.post("/", middlewares.wrap(require("./create-gift-card").default)) + + route.get("/:id", middlewares.wrap(require("./get-gift-card").default)) + + route.post("/:id", middlewares.wrap(require("./update-gift-card").default)) + + route.delete("/:id", middlewares.wrap(require("./delete-gift-card").default)) + + return app +} + +export const defaultFields = [ + "id", + "code", + "value", + "balance", + "region_id", + "is_disabled", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const defaultRelations = ["region"] + +export const allowedFields = [ + "id", + "code", + "value", + "balance", + "region_id", + "is_disabled", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const allowedRelations = ["region"] diff --git a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js new file mode 100644 index 0000000000..d54bab12e9 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js @@ -0,0 +1,19 @@ +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + try { + const selector = {} + + const giftCardService = req.scope.resolve("giftCardService") + + const giftCards = await giftCardService.list(selector, { + select: defaultFields, + relations: defaultRelations, + order: { created_at: "DESC" }, + }) + + res.status(200).json({ gift_cards: giftCards }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js b/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js new file mode 100644 index 0000000000..a225590120 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js @@ -0,0 +1,35 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + balance: Validator.number() + .precision(0) + .optional(), + ends_at: Validator.date().optional(), + is_disabled: Validator.boolean().optional(), + region_id: Validator.string().optional(), + metadata: Validator.object().optional(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const giftCardService = req.scope.resolve("giftCardService") + + await giftCardService.update(id, value) + + const giftCard = await giftCardService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ gift_card: giftCard }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index ea345ee3f7..433575eff5 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -9,11 +9,15 @@ import regionRoutes from "./regions" import shippingOptionRoutes from "./shipping-options" import shippingProfileRoutes from "./shipping-profiles" import discountRoutes from "./discounts" +import giftCardRoutes from "./gift-cards" import orderRoutes from "./orders" import storeRoutes from "./store" import uploadRoutes from "./uploads" import customerRoutes from "./customers" import appRoutes from "./apps" +import swapRoutes from "./swaps" +import returnRoutes from "./returns" +import variantRoutes from "./variants" const route = Router() @@ -48,10 +52,14 @@ export default (app, container, config) => { shippingOptionRoutes(route) shippingProfileRoutes(route) discountRoutes(route) + giftCardRoutes(route) orderRoutes(route) storeRoutes(route) uploadRoutes(route) customerRoutes(route) + swapRoutes(route) + returnRoutes(route) + variantRoutes(route) return app } diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/archive-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/archive-order.js index 7901bb45bc..9ca2108a6e 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/archive-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/archive-order.js @@ -33,7 +33,7 @@ describe("POST /admin/orders/:id/archive", () => { it("returns order with status = archived", () => { expect(subject.status).toEqual(200) - expect(subject.body.order._id).toEqual(IdMap.getId("processed-order")) + expect(subject.body.order.id).toEqual(IdMap.getId("processed-order")) expect(subject.body.order.status).toEqual("archived") }) }) diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/cancel-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/cancel-order.js index 744e4fe0dd..27d94ba353 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/cancel-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/cancel-order.js @@ -33,7 +33,7 @@ describe("POST /admin/orders/:id/cancel", () => { it("returns order with status = cancelled", () => { expect(subject.status).toEqual(200) - expect(subject.body.order._id).toEqual(IdMap.getId("test-order")) + expect(subject.body.order.id).toEqual(IdMap.getId("test-order")) expect(subject.body.order.status).toEqual("cancelled") }) }) diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/capture-payment.js b/packages/medusa/src/api/routes/admin/orders/__tests__/capture-payment.js index 416a4b88a1..427040ef9d 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/capture-payment.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/capture-payment.js @@ -33,7 +33,7 @@ describe("POST /admin/orders/:id/capture", () => { it("returns order with payment_status = captured", () => { expect(subject.status).toEqual(200) - expect(subject.body.order._id).toEqual(IdMap.getId("test-order")) + expect(subject.body.order.id).toEqual(IdMap.getId("test-order")) expect(subject.body.order.payment_status).toEqual("captured") }) }) diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/create-fulfillment.js b/packages/medusa/src/api/routes/admin/orders/__tests__/create-fulfillment.js index aad1ea64ff..971eb227e6 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/create-fulfillment.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/create-fulfillment.js @@ -48,7 +48,7 @@ describe("POST /admin/orders/:id/fulfillment", () => { it("returns order with fulfillment_status = fulfilled", () => { expect(subject.status).toEqual(200) - expect(subject.body.order._id).toEqual(IdMap.getId("test-order")) + expect(subject.body.order.id).toEqual(IdMap.getId("test-order")) expect(subject.body.order.fulfillment_status).toEqual("fulfilled") }) }) 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 ee25953eb0..58eacfd665 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 @@ -2,6 +2,53 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { OrderServiceMock } from "../../../../../services/__mocks__/order" +const defaultRelations = [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", +] + +const defaultFields = [ + "id", + "status", + "fulfillment_status", + "payment_status", + "display_id", + "cart_id", + "customer_id", + "email", + "region_id", + "currency_code", + "tax_rate", + "canceled_at", + "created_at", + "updated_at", + "metadata", + "items.refundable", + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + "refundable_amount", +] + describe("GET /admin/orders", () => { describe("successfully gets an order", () => { let subject @@ -27,13 +74,17 @@ describe("GET /admin/orders", () => { it("calls orderService retrieve", () => { expect(OrderServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(OrderServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("test-order") + IdMap.getId("test-order"), + { + select: defaultFields, + relations: defaultRelations, + } ) }) it("returns order", () => { expect(subject.status).toEqual(200) - expect(subject.body.order._id).toEqual(IdMap.getId("test-order")) + expect(subject.body.order.id).toEqual(IdMap.getId("test-order")) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js index 040f8826a8..a9e6d328d8 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js @@ -1,6 +1,7 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" -import { OrderServiceMock } from "../../../../../services/__mocks__/order" +import { orders } from "../../../../../services/__mocks__/order" +import { ReturnService } from "../../../../../services/__mocks__/return" describe("POST /admin/orders/:id/return", () => { describe("successfully returns full order", () => { @@ -19,6 +20,7 @@ describe("POST /admin/orders/:id/return", () => { quantity: 10, }, ], + refund: 10, }, adminSession: { jwt: { @@ -34,17 +36,21 @@ describe("POST /admin/orders/:id/return", () => { }) it("calls OrderService return", () => { - expect(OrderServiceMock.requestReturn).toHaveBeenCalledTimes(1) - expect(OrderServiceMock.requestReturn).toHaveBeenCalledWith( - IdMap.getId("test-order"), - [ - { - item_id: IdMap.getId("existingLine"), - quantity: 10, - }, - ], - undefined, // no shipping method - undefined // no refund amount + expect(ReturnService.create).toHaveBeenCalledTimes(1) + expect(ReturnService.create).toHaveBeenCalledWith( + { + order_id: IdMap.getId("test-order"), + idempotency_key: "testkey", + items: [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + refund_amount: 10, + shipping_method: undefined, + }, + orders.testOrder ) }) }) @@ -81,18 +87,134 @@ describe("POST /admin/orders/:id/return", () => { }) it("calls OrderService return", () => { - expect(OrderServiceMock.requestReturn).toHaveBeenCalledTimes(1) - expect(OrderServiceMock.requestReturn).toHaveBeenCalledWith( - IdMap.getId("test-order"), - [ - { - item_id: IdMap.getId("existingLine"), - quantity: 10, - }, - ], - undefined, // no shipping method - 0 + expect(ReturnService.create).toHaveBeenCalledTimes(1) + expect(ReturnService.create).toHaveBeenCalledWith( + { + order_id: IdMap.getId("test-order"), + idempotency_key: "testkey", + items: [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + refund_amount: 0, + shipping_method: undefined, + }, + orders.testOrder ) }) }) + + describe("defaults to 0 on negative refund amount", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request( + "POST", + `/admin/orders/${IdMap.getId("test-order")}/return`, + { + payload: { + items: [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + refund: -1, + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls OrderService return", () => { + expect(ReturnService.create).toHaveBeenCalledTimes(1) + expect(ReturnService.create).toHaveBeenCalledWith( + { + order_id: IdMap.getId("test-order"), + idempotency_key: "testkey", + items: [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + refund_amount: 0, + shipping_method: undefined, + }, + orders.testOrder + ) + }) + }) + + describe("fulfills", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request( + "POST", + `/admin/orders/${IdMap.getId("test-order")}/return`, + { + payload: { + items: [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + refund: 100, + return_shipping: { + option_id: "opt_1234", + price: 12, + }, + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls OrderService return", () => { + expect(ReturnService.create).toHaveBeenCalledTimes(1) + expect(ReturnService.create).toHaveBeenCalledWith( + { + order_id: IdMap.getId("test-order"), + idempotency_key: "testkey", + items: [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + refund_amount: 100, + shipping_method: { + option_id: "opt_1234", + price: 12, + }, + }, + orders.testOrder + ) + + expect(ReturnService.fulfill).toHaveBeenCalledTimes(1) + expect(ReturnService.fulfill).toHaveBeenCalledWith("return") + }) + }) }) diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/set-metadata.js b/packages/medusa/src/api/routes/admin/orders/__tests__/set-metadata.js deleted file mode 100644 index 4d539f4f51..0000000000 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/set-metadata.js +++ /dev/null @@ -1,40 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { OrderServiceMock } from "../../../../../services/__mocks__/order" - -describe("POST /admin/orders/:id/metadata", () => { - describe("successfully sets metadata on order", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/orders/${IdMap.getId("test-order")}/metadata`, - { - payload: { - key: "Test key", - value: "Test value", - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls OrderService setMetadata", () => { - expect(OrderServiceMock.setMetadata).toHaveBeenCalledTimes(1) - expect(OrderServiceMock.setMetadata).toHaveBeenCalledWith( - IdMap.getId("test-order"), - "Test key", - "Test value" - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/orders/add-shipping-method.js b/packages/medusa/src/api/routes/admin/orders/add-shipping-method.js new file mode 100644 index 0000000000..dbf7b084ee --- /dev/null +++ b/packages/medusa/src/api/routes/admin/orders/add-shipping-method.js @@ -0,0 +1,41 @@ +import _ from "lodash" +import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + price: Validator.number() + .integer() + .integer() + .allow(0) + .required(), + option_id: Validator.string().required(), + data: Validator.object() + .optional() + .default({}), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const orderService = req.scope.resolve("orderService") + + await orderService.addShippingMethod(id, value.option_id, value.data, { + price: value.price, + }) + + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ order }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/orders/archive-order.js b/packages/medusa/src/api/routes/admin/orders/archive-order.js index f788035f68..f4769a6af3 100644 --- a/packages/medusa/src/api/routes/admin/orders/archive-order.js +++ b/packages/medusa/src/api/routes/admin/orders/archive-order.js @@ -3,12 +3,13 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.archive(id) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + await orderService.archive(id) + + const order = await orderService.retrieve(id, { + relations: ["region", "customer", "swaps"], + }) + res.json({ order }) } catch (error) { throw error diff --git a/packages/medusa/src/api/routes/admin/orders/cancel-order.js b/packages/medusa/src/api/routes/admin/orders/cancel-order.js index 3ef00cffa7..a16d865ce2 100644 --- a/packages/medusa/src/api/routes/admin/orders/cancel-order.js +++ b/packages/medusa/src/api/routes/admin/orders/cancel-order.js @@ -3,12 +3,13 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.cancel(id) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + await orderService.cancel(id) + + const order = await orderService.retrieve(id, { + relations: ["region", "customer", "swaps"], + }) + res.json({ order }) } catch (error) { throw error diff --git a/packages/medusa/src/api/routes/admin/orders/capture-payment.js b/packages/medusa/src/api/routes/admin/orders/capture-payment.js index d78ccf7c1a..07a9126705 100644 --- a/packages/medusa/src/api/routes/admin/orders/capture-payment.js +++ b/packages/medusa/src/api/routes/admin/orders/capture-payment.js @@ -1,14 +1,18 @@ +import { defaultRelations, defaultFields } from "./" + export default async (req, res) => { const { id } = req.params try { const orderService = req.scope.resolve("orderService") - let order = await orderService.capturePayment(id) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + await orderService.capturePayment(id) + + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + res.json({ order }) } catch (error) { throw error diff --git a/packages/medusa/src/api/routes/admin/orders/complete-order.js b/packages/medusa/src/api/routes/admin/orders/complete-order.js index 64c18eaa8b..7549b61e02 100644 --- a/packages/medusa/src/api/routes/admin/orders/complete-order.js +++ b/packages/medusa/src/api/routes/admin/orders/complete-order.js @@ -3,12 +3,13 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.completeOrder(id) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + await orderService.completeOrder(id) + + const order = await orderService.retrieve(id, { + relations: ["region", "customer", "swaps"], + }) + res.json({ order }) } catch (error) { throw error diff --git a/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js b/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js index 3e90c0e551..8e918e915a 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id } = req.params @@ -21,19 +22,14 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.createFulfillment( - id, - value.items, - value.metadata - ) + await orderService.createFulfillment(id, value.items, value.metadata) - const data = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - res.json({ order: data }) + res.json({ order }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/orders/create-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-shipment.js index b29e4c0901..24e0ebe199 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-shipment.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id } = req.params @@ -17,16 +18,18 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.createShipment( + + await orderService.createShipment( id, value.fulfillment_id, value.tracking_numbers ) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + res.json({ order }) } catch (error) { throw error diff --git a/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js index b6a4cc672c..700e1599e0 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, swap_id } = req.params @@ -19,22 +20,18 @@ export default async (req, res) => { const orderService = req.scope.resolve("orderService") const swapService = req.scope.resolve("swapService") - const order = await orderService.retrieve(id) - await swapService.createShipment( swap_id, value.fulfillment_id, value.tracking_numbers ) - // Decorate the order - const data = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - res.json({ order: data }) + res.json({ order }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/orders/create-swap.js b/packages/medusa/src/api/routes/admin/orders/create-swap.js index 095829f3c3..6910966c09 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-swap.js +++ b/packages/medusa/src/api/routes/admin/orders/create-swap.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params @@ -12,8 +13,10 @@ export default async (req, res) => { .required(), return_shipping: Validator.object() .keys({ - id: Validator.string().optional(), - price: Validator.number().optional(), + option_id: Validator.string().optional(), + price: Validator.number() + .integer() + .optional(), }) .optional(), additional_items: Validator.array().items({ @@ -27,41 +30,141 @@ export default async (req, res) => { throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) } + const idempotencyKeyService = req.scope.resolve("idempotencyKeyService") + + const headerKey = req.get("Idempotency-Key") || "" + + let idempotencyKey + try { + idempotencyKey = await idempotencyKeyService.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 { const orderService = req.scope.resolve("orderService") const swapService = req.scope.resolve("swapService") + const returnService = req.scope.resolve("returnService") - let order = await orderService.retrieve(id) + let inProgress = true + let err = false - // Phase 1: Create swap and add it to the order - const swap = await swapService.create( - order, - value.return_items, - value.additional_items, - value.return_shipping - ) + while (inProgress) { + switch (idempotencyKey.recovery_point) { + case "started": { + const { key, error } = await idempotencyKeyService.workStage( + idempotencyKey.idempotency_key, + async manager => { + const order = await orderService + .withTransaction(manager) + .retrieve(id, { + select: ["refunded_total", "total"], + relations: ["items", "swaps"], + }) - await orderService.registerSwapCreated(id, swap._id) + const swap = await swapService + .withTransaction(manager) + .create( + order, + value.return_items, + value.additional_items, + value.return_shipping, + { idempotency_key: idempotencyKey.idempotency_key } + ) - // --> swap_created - // Phase 2: Create a return request from the swap - await swapService.requestReturn(order, swap._id) + await swapService.withTransaction(manager).createCart(swap.id) + const returnOrder = await returnService + .withTransaction(manager) + .retrieveBySwap(swap.id) - // --> return_request_created - // Phase 3: Create a cart that can be used to pay for the swap difference - await swapService.createCart(order, swap._id) + await returnService + .withTransaction(manager) + .fulfill(returnOrder.id) - // --> finished + return { + recovery_point: "swap_created", + } + } + ) - order = await orderService.retrieve(id) - const data = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + break + } - res.status(200).json({ order: data }) - } catch (err) { - throw err + case "swap_created": { + const { key, error } = await idempotencyKeyService.workStage( + idempotencyKey.idempotency_key, + async manager => { + const swaps = await swapService.list({ + idempotency_key: idempotencyKey.idempotency_key, + }) + + if (!swaps.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Swap not found" + ) + } + + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + return { + response_code: 200, + response_body: { order }, + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + break + } + + case "finished": { + inProgress = false + break + } + + default: + idempotencyKey = await idempotencyKeyService.update( + idempotencyKey.idempotency_key, + { + recovery_point: "finished", + response_code: 500, + response_body: { message: "Unknown recovery point" }, + } + ) + break + } + } + + if (err) { + throw err + } + + res.status(idempotencyKey.response_code).json(idempotencyKey.response_body) + } catch (error) { + throw error } } diff --git a/packages/medusa/src/api/routes/admin/orders/delete-metadata.js b/packages/medusa/src/api/routes/admin/orders/delete-metadata.js index 791e650b72..9267fea4d5 100644 --- a/packages/medusa/src/api/routes/admin/orders/delete-metadata.js +++ b/packages/medusa/src/api/routes/admin/orders/delete-metadata.js @@ -3,8 +3,12 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.deleteMetadata(id, key) - order = await orderService.decorate(order, [], ["region"]) + + await orderService.deleteMetadata(id, key) + + const order = await orderService.retrieve(id, { + relations: ["region", "customer", "swaps"], + }) res.status(200).json({ order }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/orders/fulfill-swap.js b/packages/medusa/src/api/routes/admin/orders/fulfill-swap.js index a619ecba32..f169507545 100644 --- a/packages/medusa/src/api/routes/admin/orders/fulfill-swap.js +++ b/packages/medusa/src/api/routes/admin/orders/fulfill-swap.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id, swap_id } = req.params @@ -15,21 +16,20 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") const swapService = req.scope.resolve("swapService") + const entityManager = req.scope.resolve("manager") - // Fetch the order - const order = await orderService.retrieve(id) + await entityManager.transaction(async manager => { + await swapService + .withTransaction(manager) + .createFulfillment(swap_id, value.metadata) - // Receive the return - await swapService.createFulfillment(order, swap_id, value.metadata) + const order = await orderService.withTransaction(manager).retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - // Decorate the order - const data = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) - - res.status(200).json({ order: data }) + res.status(200).json({ order }) + }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/orders/get-order.js b/packages/medusa/src/api/routes/admin/orders/get-order.js index 5e224c9e0d..ea3b752132 100644 --- a/packages/medusa/src/api/routes/admin/orders/get-order.js +++ b/packages/medusa/src/api/routes/admin/orders/get-order.js @@ -1,16 +1,15 @@ +import { defaultRelations, defaultFields } from "./" + export default async (req, res) => { const { id } = req.params try { const orderService = req.scope.resolve("orderService") - const customerService = req.scope.resolve("customerService") - let order = await orderService.retrieve(id) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.json({ order }) } catch (error) { diff --git a/packages/medusa/src/api/routes/admin/orders/index.js b/packages/medusa/src/api/routes/admin/orders/index.js index 1790844721..04b044ce48 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.js +++ b/packages/medusa/src/api/routes/admin/orders/index.js @@ -87,6 +87,14 @@ export default app => { */ route.post("/:id/cancel", middlewares.wrap(require("./cancel-order").default)) + /** + * Add a shipping method + */ + route.post( + "/:id/shipping-methods", + middlewares.wrap(require("./add-shipping-method").default) + ) + /** * Archive an order. */ @@ -132,14 +140,6 @@ export default app => { middlewares.wrap(require("./process-swap-payment").default) ) - /** - * Set metadata key / value pair. - */ - route.post( - "/:id/metadata", - middlewares.wrap(require("./set-metadata").default) - ) - /** * Delete metadata key / value pair. */ @@ -150,3 +150,91 @@ export default app => { return app } + +export const defaultRelations = [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "payments", + "fulfillments", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", +] + +export const defaultFields = [ + "id", + "status", + "fulfillment_status", + "payment_status", + "display_id", + "cart_id", + "customer_id", + "email", + "region_id", + "currency_code", + "tax_rate", + "canceled_at", + "created_at", + "updated_at", + "metadata", + "items.refundable", + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", + "refundable_amount", +] + +export const allowedFields = [ + "id", + "status", + "fulfillment_status", + "payment_status", + "display_id", + "cart_id", + "customer_id", + "email", + "region_id", + "currency_code", + "tax_rate", + "canceled_at", + "created_at", + "updated_at", + "metadata", + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "subtotal", + "gift_card_total", + "total", + "refundable_amount", +] + +export const allowedRelations = [ + "customer", + "region", + "billing_address", + "shipping_address", + "discounts", + "shipping_methods", + "payments", + "fulfillments", + "returns", + "swaps", + "swaps.return_order", + "swaps.additional_items", +] diff --git a/packages/medusa/src/api/routes/admin/orders/list-orders.js b/packages/medusa/src/api/routes/admin/orders/list-orders.js index 661acb4f7e..d76b9fb2e0 100644 --- a/packages/medusa/src/api/routes/admin/orders/list-orders.js +++ b/packages/medusa/src/api/routes/admin/orders/list-orders.js @@ -1,80 +1,61 @@ import _ from "lodash" +import { Not } from "typeorm" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - const queryBuilderService = req.scope.resolve("queryBuilderService") - - let query = queryBuilderService.buildQuery(req.query, [ - "display_id", - "email", - "status", - "fulfillment_status", - "payment_status", - ]) const limit = parseInt(req.query.limit) || 50 const offset = parseInt(req.query.offset) || 0 - let orders + let selector = {} - // Temporary solution for admin tabs filtering - // Will be replaced in Mongo -> Postgres migration - if (req.query["status"]) { - if (req.query["status"] === "returns") { - query = { - "returns.status": "requested", - } - } - - if (req.query["status"] === "new") { - query = { - $or: [ - { fulfillment_status: { $ne: "shipped" } }, - { payment_status: { $ne: "captured" } }, - ], - } - } - - if (req.query["status"] === "requires_action") { - query = { - $or: [ - { status: "requires_action" }, - { payment_status: "requires_action" }, - { fulfillment_status: "requires_action" }, - ], - } - } - - if (req.query["status"] === "swaps") { - const swapService = req.scope.resolve("swapService") - - const swapsInProgress = await swapService.list({ - fulfillment_status: { $ne: "shipped" }, - }) - - if (swapsInProgress.length) { - query = { - swaps: { $in: swapsInProgress.map(s => s._id.toString()) }, - } - } - } + if ("q" in req.query) { + selector.q = req.query.q } - orders = await orderService.list(query, offset, limit) - let includeFields = [] if ("fields" in req.query) { includeFields = req.query.fields.split(",") } - orders = await Promise.all( - orders.map(order => orderService.decorate(order, includeFields)) + let expandFields = [] + if ("expand" in req.query) { + expandFields = req.query.expand.split(",") + } + + if ("new" in req.query) { + selector = { + payment_status: Not("captured"), + fulfillment_status: Not("shipped"), + } + } + + if ("requires_more" in req.query) { + selector = { + payment_status: Not("captured"), + fulfillment_status: Not("shipped"), + } + } + + const listConfig = { + select: includeFields.length ? includeFields : defaultFields, + relations: expandFields.length ? expandFields : defaultRelations, + skip: offset, + take: limit, + order: { created_at: "DESC" }, + } + + const [orders, count] = await orderService.listAndCount( + selector, + listConfig ) - let numOrders = await orderService.count() + const fields = [...includeFields, ...expandFields] + const data = orders.map(o => _.pick(o, fields)) - res.json({ orders, total_count: numOrders }) + res.json({ orders: data, count, offset, limit }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/orders/process-swap-payment.js b/packages/medusa/src/api/routes/admin/orders/process-swap-payment.js index b83bb35d8e..72ff23f87f 100644 --- a/packages/medusa/src/api/routes/admin/orders/process-swap-payment.js +++ b/packages/medusa/src/api/routes/admin/orders/process-swap-payment.js @@ -1,22 +1,22 @@ +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, swap_id } = req.params try { const orderService = req.scope.resolve("orderService") const swapService = req.scope.resolve("swapService") + const entityManager = req.scope.resolve("manager") - const order = await orderService.retrieve(id) + await entityManager.transaction(async manager => { + await swapService.withTransaction(manager).processDifference(swap_id) - await swapService.processDifference(swap_id) + const order = await orderService.withTransaction(manager).retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - // Decorate the order - const data = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) - - res.json({ order: data }) + res.json({ order }) + }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/orders/receive-return.js b/packages/medusa/src/api/routes/admin/orders/receive-return.js index 232a10883e..780bfdfa05 100644 --- a/packages/medusa/src/api/routes/admin/orders/receive-return.js +++ b/packages/medusa/src/api/routes/admin/orders/receive-return.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id, return_id } = req.params @@ -10,7 +11,9 @@ export default async (req, res) => { quantity: Validator.number().required(), }) .required(), - refund: Validator.number().optional(), + refund: Validator.number() + .integer() + .optional(), }) const { value, error } = schema.validate(req.body) @@ -22,9 +25,11 @@ export default async (req, res) => { const orderService = req.scope.resolve("orderService") let refundAmount = value.refund + if (typeof value.refund !== "undefined" && value.refund < 0) { refundAmount = 0 } + let order = await orderService.receiveReturn( id, return_id, @@ -32,11 +37,11 @@ export default async (req, res) => { refundAmount, true ) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ order }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/orders/receive-swap.js b/packages/medusa/src/api/routes/admin/orders/receive-swap.js index 6a6ae283c5..7911b20a1f 100644 --- a/packages/medusa/src/api/routes/admin/orders/receive-swap.js +++ b/packages/medusa/src/api/routes/admin/orders/receive-swap.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, swap_id } = req.params @@ -20,24 +21,24 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") const swapService = req.scope.resolve("swapService") + const entityManager = req.scope.resolve("manager") - // Fetch the order - let order = await orderService.retrieve(id) + await entityManager.transaction(async manager => { + await swapService + .withTransaction(manager) + .receiveReturn(swap_id, value.items) - // Receive the return - await swapService.receiveReturn(order, swap_id, value.items) + await orderService + .withTransaction(manager) + .registerSwapReceived(id, swap_id) + }) - // Register swap reception - order = await orderService.registerSwapReceived(id, swap_id) + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - // Decorate the order - const data = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) - - res.status(200).json({ order: data }) + res.status(200).json({ order }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/orders/refund-payment.js b/packages/medusa/src/api/routes/admin/orders/refund-payment.js index 7ae16b9cac..d5ae26dffe 100644 --- a/packages/medusa/src/api/routes/admin/orders/refund-payment.js +++ b/packages/medusa/src/api/routes/admin/orders/refund-payment.js @@ -1,9 +1,12 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ - amount: Validator.number().required(), + amount: Validator.number() + .integer() + .required(), reason: Validator.string().required(), note: Validator.string() .allow("") @@ -17,17 +20,13 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.createRefund( - id, - value.amount, - value.reason, - value.note - ) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + await orderService.createRefund(id, value.amount, value.reason, value.note) + + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ order }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/orders/request-return.js b/packages/medusa/src/api/routes/admin/orders/request-return.js index 7066abdba1..9cd28a5799 100644 --- a/packages/medusa/src/api/routes/admin/orders/request-return.js +++ b/packages/medusa/src/api/routes/admin/orders/request-return.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id } = req.params @@ -10,10 +11,18 @@ export default async (req, res) => { quantity: Validator.number().required(), }) .required(), - shipping_method: Validator.string().optional(), - shipping_price: Validator.number().optional(), + return_shipping: Validator.object() + .keys({ + option_id: Validator.string().optional(), + price: Validator.number() + .integer() + .optional(), + }) + .optional(), receive_now: Validator.boolean().default(false), - refund: Validator.number().optional(), + refund: Validator.number() + .integer() + .optional(), }) const { value, error } = schema.validate(req.body) @@ -21,59 +30,166 @@ export default async (req, res) => { throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) } + const idempotencyKeyService = req.scope.resolve("idempotencyKeyService") + + const headerKey = req.get("Idempotency-Key") || "" + + let idempotencyKey + try { + idempotencyKey = await idempotencyKeyService.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 { const orderService = req.scope.resolve("orderService") + const returnService = req.scope.resolve("returnService") - let oldOrder - let existingReturns = [] - if (value.receive_now) { - oldOrder = await orderService.retrieve(id) - existingReturns = oldOrder.returns.map(r => r._id) - } + let inProgress = true + let err = false - let shippingMethod - if (value.shipping_method) { - shippingMethod = { - id: value.shipping_method, - price: value.shipping_price, + while (inProgress) { + switch (idempotencyKey.recovery_point) { + case "started": { + const { key, error } = await idempotencyKeyService.workStage( + idempotencyKey.idempotency_key, + async manager => { + const order = await orderService + .withTransaction(manager) + .retrieve(id, { + select: ["refunded_total", "total"], + relations: ["items"], + }) + + const returnObj = { + order_id: id, + idempotency_key: idempotencyKey.idempotency_key, + items: value.items, + } + + if (value.return_shipping) { + returnObj.shipping_method = value.return_shipping + } + + if (typeof value.refund !== "undefined" && value.refund < 0) { + returnObj.refund_amount = 0 + } else { + if (value.refund) { + returnObj.refund_amount = value.refund + } + } + + const createdReturn = await returnService + .withTransaction(manager) + .create(returnObj, order) + + if (value.return_shipping) { + await returnService + .withTransaction(manager) + .fulfill(createdReturn.id) + } + + return { + recovery_point: "return_requested", + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + break + } + + case "return_requested": { + const { key, error } = await idempotencyKeyService.workStage( + idempotencyKey.idempotency_key, + async manager => { + let order = await orderService + .withTransaction(manager) + .retrieve(id, { relations: ["returns"] }) + + /** + * If we are ready to receive immediately, we find the newly created return + * and register it as received. + */ + if (value.receive_now) { + let ret = await returnService.withTransaction(manager).list({ + idempotency_key: idempotencyKey.idempotency_key, + }) + + if (!ret.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Return not found` + ) + } + + ret = ret[0] + + order = await returnService + .withTransaction(manager) + .receiveReturn(order.id, ret.id, value.items, value.refund) + } + + order = await orderService.withTransaction(manager).retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + return { + response_code: 200, + response_body: { order }, + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + break + } + + case "finished": { + inProgress = false + break + } + + default: + idempotencyKey = await idempotencyKeyService.update( + idempotencyKey.idempotency_key, + { + recovery_point: "finished", + response_code: 500, + response_body: { message: "Unknown recovery point" }, + } + ) + break } } - let refundAmount = value.refund - if (typeof value.refund !== "undefined" && value.refund < 0) { - refundAmount = 0 - } - let order = await orderService.requestReturn( - id, - value.items, - shippingMethod, - refundAmount - ) - - /** - * If we are ready to receive immediately, we find the newly created return - * and register it as received. - */ - if (value.receive_now) { - const newReturn = order.returns.find( - r => !existingReturns.includes(r._id) - ) - order = await orderService.return( - id, - newReturn._id, - value.items, - value.refund - ) + if (err) { + throw err } - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) - - res.status(200).json({ order }) + res.status(idempotencyKey.response_code).json(idempotencyKey.response_body) } catch (err) { + console.log(err) throw err } } diff --git a/packages/medusa/src/api/routes/admin/orders/update-order.js b/packages/medusa/src/api/routes/admin/orders/update-order.js index 73f3392620..37e321024b 100644 --- a/packages/medusa/src/api/routes/admin/orders/update-order.js +++ b/packages/medusa/src/api/routes/admin/orders/update-order.js @@ -1,12 +1,13 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "." export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ email: Validator.string().email(), - billing_address: Validator.address(), - shipping_address: Validator.address(), + billing_address: Validator.object(), + shipping_address: Validator.object(), items: Validator.array(), region: Validator.string(), discounts: Validator.array(), @@ -31,12 +32,13 @@ export default async (req, res) => { try { const orderService = req.scope.resolve("orderService") - let order = await orderService.update(id, value) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + + await orderService.update(id, value) + + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ order }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js b/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js index c05f6f91da..edf3fd1f66 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js @@ -36,10 +36,7 @@ describe("POST /admin/products/:id/options", () => { }) it("returns the updated product decorated", () => { - expect(subject.body.product._id).toEqual( - IdMap.getId("productWithOptions") - ) - expect(subject.body.product.decorated).toEqual(true) + expect(subject.body.product.id).toEqual(IdMap.getId("productWithOptions")) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js index 042aa5aed2..4dc9cc6cf9 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js @@ -28,18 +28,18 @@ describe("POST /admin/products", () => { }) it("returns created product draft", () => { - expect(subject.body.product._id).toEqual(IdMap.getId("product1")) - expect(subject.body.product.decorated).toEqual(true) + expect(subject.body.product.id).toEqual(IdMap.getId("product1")) }) it("calls service createDraft", () => { - expect(ProductServiceMock.createDraft).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.createDraft).toHaveBeenCalledWith({ + expect(ProductServiceMock.create).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.create).toHaveBeenCalledWith({ title: "Test Product", description: "Test Description", tags: "hi,med,dig", handle: "test-product", is_giftcard: false, + profile_id: IdMap.getId("default_shipping_profile"), }) }) @@ -89,13 +89,14 @@ describe("POST /admin/products", () => { }) it("calls service createDraft", () => { - expect(ProductServiceMock.createDraft).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.createDraft).toHaveBeenCalledWith({ + expect(ProductServiceMock.create).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.create).toHaveBeenCalledWith({ title: "Gift Card", description: "make someone happy", options: [{ title: "Denominations" }], handle: "test-gift-card", is_giftcard: true, + profile_id: IdMap.getId("giftCardProfile"), }) }) @@ -106,12 +107,6 @@ describe("POST /admin/products", () => { expect( ShippingProfileServiceMock.retrieveGiftCardDefault ).toHaveBeenCalledWith() - - expect(ShippingProfileServiceMock.addProduct).toHaveBeenCalledTimes(1) - expect(ShippingProfileServiceMock.addProduct).toHaveBeenCalledWith( - IdMap.getId("giftCardProfile"), - undefined - ) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js index 5e5494b4cc..6063071ecb 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js @@ -1,6 +1,6 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" -import { ProductServiceMock } from "../../../../../services/__mocks__/product" +import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" describe("POST /admin/products/:id/variants", () => { describe("successful add variant", () => { @@ -34,10 +34,11 @@ describe("POST /admin/products/:id/variants", () => { }) it("calls service addVariant", () => { - expect(ProductServiceMock.createVariant).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.createVariant).toHaveBeenCalledWith( + expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1) + expect(ProductVariantServiceMock.create).toHaveBeenCalledWith( IdMap.getId("productWithOptions"), { + inventory_quantity: 0, title: "Test Product Variant", options: [], prices: [ @@ -51,10 +52,7 @@ describe("POST /admin/products/:id/variants", () => { }) it("returns the updated product decorated", () => { - expect(subject.body.product._id).toEqual( - IdMap.getId("productWithOptions") - ) - expect(subject.body.product.decorated).toEqual(true) + expect(subject.body.product.id).toEqual(IdMap.getId("productWithOptions")) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js index 5897497efa..925f17e6d4 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js @@ -1,6 +1,6 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" -import { ProductServiceMock } from "../../../../../services/__mocks__/product" +import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" describe("POST /admin/products/:id/variants/:variantId", () => { describe("successful removes variant", () => { @@ -27,9 +27,8 @@ describe("POST /admin/products/:id/variants/:variantId", () => { }) it("calls service removeVariant", () => { - expect(ProductServiceMock.deleteVariant).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.deleteVariant).toHaveBeenCalledWith( - IdMap.getId("productWithOptions"), + expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(1) + expect(ProductVariantServiceMock.delete).toHaveBeenCalledWith( IdMap.getId("variant1") ) }) @@ -39,19 +38,7 @@ describe("POST /admin/products/:id/variants/:variantId", () => { variant_id: IdMap.getId("variant1"), object: "product-variant", deleted: true, - product: { - _id: IdMap.getId("productWithOptions"), - decorated: true, - options: [ - { - _id: IdMap.getId("option1"), - title: "Test", - values: [IdMap.getId("optionValue1")], - }, - ], - title: "Test", - variants: [IdMap.getId("variant1")], - }, + product: expect.any(Object), }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js index e6ae586169..d63a6129ba 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js @@ -27,13 +27,43 @@ describe("GET /admin/products/:id", () => { it("calls get product from productSerice", () => { expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(ProductServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("product1") + IdMap.getId("product1"), + { + select: [ + "id", + "title", + "subtitle", + "description", + "tags", + "handle", + "is_giftcard", + "thumbnail", + "profile_id", + "weight", + "length", + "height", + "width", + "hs_code", + "origin_country", + "mid_code", + "material", + "created_at", + "updated_at", + "metadata", + ], + relations: [ + "variants", + "variants.prices", + "variants.options", + "images", + "options", + ], + } ) }) it("returns product decorated", () => { - expect(subject.body.product._id).toEqual(IdMap.getId("product1")) - expect(subject.body.product.decorated).toEqual(true) + expect(subject.body.product.id).toEqual(IdMap.getId("product1")) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js deleted file mode 100644 index c727cf1dd8..0000000000 --- a/packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js +++ /dev/null @@ -1,39 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductServiceMock } from "../../../../../services/__mocks__/product" - -describe("GET /admin/products/:id/variants", () => { - describe("successfully gets a product", () => { - let subject - - beforeAll(async () => { - subject = await request( - "GET", - `/admin/products/${IdMap.getId("product1")}/variants`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls get product from productSerice", () => { - expect(ProductServiceMock.retrieveVariants).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.retrieveVariants).toHaveBeenCalledWith( - IdMap.getId("product1") - ) - }) - - it("returns variants", () => { - expect(subject.body.variants[0]._id).toEqual(IdMap.getId("1")) - expect(subject.body.variants[1]._id).toEqual(IdMap.getId("2")) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js deleted file mode 100644 index b3e5ed31e5..0000000000 --- a/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js +++ /dev/null @@ -1,38 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductServiceMock } from "../../../../../services/__mocks__/product" - -describe("POST /admin/products/:id/publish", () => { - describe("successful publish", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/products/${IdMap.getId("publish")}/publish`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("returns product with published flag true", () => { - expect(subject.body.product.published).toEqual(true) - }) - - it("calls service publish", () => { - expect(ProductServiceMock.publish).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.publish).toHaveBeenCalledWith( - IdMap.getId("publish") - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js index 61c5632fe5..efb7aec556 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js @@ -1,6 +1,5 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" -import { ProductServiceMock } from "../../../../../services/__mocks__/product" import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" describe("POST /admin/products/:id/variants/:variantId", () => { @@ -41,140 +40,28 @@ describe("POST /admin/products/:id/variants/:variantId", () => { expect(subject.status).toEqual(200) }) - it("calls service removeVariant", () => { - expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledTimes( - 1 - ) - expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledWith( - IdMap.getId("variant1"), - "DKK", - 100, - undefined - ) - - expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledWith( - IdMap.getId("variant1"), - IdMap.getId("region-fr"), - 100, - undefined - ) - }) - it("filters prices", () => { expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1) expect(ProductVariantServiceMock.update).toHaveBeenCalledWith( IdMap.getId("variant1"), { title: "hi", - } - ) - }) - - it("returns decorated product with variant removed", () => { - expect(subject.body.product._id).toEqual( - IdMap.getId("productWithOptions") - ) - expect(subject.body.product.decorated).toEqual(true) - }) - }) - - describe("successful updates options", () => { - let subject - - beforeAll(async () => { - jest.clearAllMocks() - subject = await request( - "POST", - `/admin/products/${IdMap.getId( - "productWithOptions" - )}/variants/${IdMap.getId("variant1")}`, - { - payload: { - options: [ - { - option_id: IdMap.getId("option_id"), - value: 100, - }, - ], - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), + prices: [ + { + region_id: IdMap.getId("region-fr"), + amount: 100, }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service removeVariant", () => { - expect(ProductServiceMock.updateOptionValue).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.updateOptionValue).toHaveBeenCalledWith( - IdMap.getId("productWithOptions"), - IdMap.getId("variant1"), - IdMap.getId("option_id"), - 100 - ) - }) - - it("returns decorated product with variant removed", () => { - expect(subject.body.product._id).toEqual( - IdMap.getId("productWithOptions") - ) - expect(subject.body.product.decorated).toEqual(true) - }) - }) - - describe("successful updates variant", () => { - let subject - - beforeAll(async () => { - jest.clearAllMocks() - subject = await request( - "POST", - `/admin/products/${IdMap.getId( - "productWithOptions" - )}/variants/${IdMap.getId("variant1")}`, - { - payload: { - title: "hi", - inventory_quantity: 123, - allow_backorder: true, - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), + { + currency_code: "DKK", + amount: 100, }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls variant update", () => { - expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.update).toHaveBeenCalledWith( - IdMap.getId("variant1"), - { - title: "hi", - inventory_quantity: 123, - allow_backorder: true, + ], } ) }) it("returns decorated product with variant removed", () => { - expect(subject.body.product._id).toEqual( - IdMap.getId("productWithOptions") - ) - expect(subject.body.product.decorated).toEqual(true) + expect(subject.body.product.id).toEqual(IdMap.getId("productWithOptions")) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/add-option.js b/packages/medusa/src/api/routes/admin/products/add-option.js index 26960b7b13..ab9cc1049a 100644 --- a/packages/medusa/src/api/routes/admin/products/add-option.js +++ b/packages/medusa/src/api/routes/admin/products/add-option.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { id } = req.params @@ -13,23 +14,14 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const newProduct = await productService.addOption(id, value.title) - const data = await productService.decorate( - newProduct, - [ - "title", - "description", - "tags", - "handle", - "images", - "thumbnail", - "options", - "published", - ], - ["variants"] - ) - res.json({ product: data }) + await productService.addOption(id, value.title) + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ product }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/create-product.js b/packages/medusa/src/api/routes/admin/products/create-product.js index 2164713b39..62d13e52ab 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/create-product.js @@ -1,40 +1,85 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "." export default async (req, res) => { const schema = Validator.object().keys({ title: Validator.string().required(), + subtitle: Validator.string().allow(""), description: Validator.string().allow(""), - tags: Validator.string(), + tags: Validator.string().optional(), is_giftcard: Validator.boolean().default(false), + images: Validator.array() + .items(Validator.string()) + .optional(), + thumbnail: Validator.string().optional(), + handle: Validator.string().optional(), options: Validator.array().items({ title: Validator.string().required(), }), - images: Validator.array().items(Validator.string()), - thumbnail: Validator.string().optional(), variants: Validator.array().items({ title: Validator.string().required(), - sku: Validator.string(), - ean: Validator.string(), - barcode: Validator.string(), + sku: Validator.string().allow(null), + ean: Validator.string().allow(null), + upc: Validator.string().allow(null), + barcode: Validator.string().allow(null), + hs_code: Validator.string().allow(null), + inventory_quantity: Validator.number().default(0), + allow_backorder: Validator.boolean().optional(), + manage_inventory: Validator.boolean().optional(), + weight: Validator.number().optional(), + length: Validator.number().optional(), + height: Validator.number().optional(), + width: Validator.number().optional(), + origin_country: Validator.string() + .optional() + .allow("") + .allow(null), + mid_code: Validator.string() + .optional() + .allow("") + .allow(null), + material: Validator.string() + .optional() + .allow("") + .allow(null), + metadata: Validator.object().optional(), prices: Validator.array() - .items({ - currency_code: Validator.string().required(), - amount: Validator.number().required(), - sale_amount: Validator.number().optional(), - }) + .items( + Validator.object() + .keys({ + region_id: Validator.string(), + currency_code: Validator.string().required(), + amount: Validator.number() + .integer() + .required(), + sale_amount: Validator.number().optional(), + }) + .xor("region_id", "currency_code") + ) .required(), options: Validator.array() .items({ value: Validator.string().required(), }) .default([]), - inventory_quantity: Validator.number().optional(), - allow_backorder: Validator.boolean().optional(), - manage_inventory: Validator.boolean().optional(), - metadata: Validator.object().optional(), }), - metadata: Validator.object(), - handle: Validator.string(), + weight: Validator.number().optional(), + length: Validator.number().optional(), + height: Validator.number().optional(), + width: Validator.number().optional(), + hs_code: Validator.string() + .optional() + .allow(""), + origin_country: Validator.string() + .optional() + .allow(""), + mid_code: Validator.string() + .optional() + .allow(""), + material: Validator.string() + .optional() + .allow(""), + metadata: Validator.object().optional(), }) const { value, error } = schema.validate(req.body) @@ -43,62 +88,62 @@ export default async (req, res) => { } try { - const { variants } = value - delete value.variants - const productService = req.scope.resolve("productService") + const productVariantService = req.scope.resolve("productVariantService") const shippingProfileService = req.scope.resolve("shippingProfileService") - if (!value.thumbnail && value.images && value.images.length) { - value.thumbnail = value.images[0] - } - let newProduct = await productService.createDraft(value) + const entityManager = req.scope.resolve("manager") - if (variants) { - const optionIds = value.options.map( - o => newProduct.options.find(newO => newO.title === o.title)._id - ) + let newProduct + await entityManager.transaction(async manager => { + const { variants } = value + delete value.variants - await Promise.all( - variants.map(v => { - const variant = { - ...v, - options: v.options.map((o, index) => ({ - ...o, - option_id: optionIds[index], - })), - } - return productService.createVariant(newProduct._id, variant) - }) - ) - } + if (!value.thumbnail && value.images && value.images.length) { + value.thumbnail = value.images[0] + } - // Add to default shipping profile - if (value.is_giftcard) { - const { _id } = await shippingProfileService.retrieveGiftCardDefault() - await shippingProfileService.addProduct(_id, newProduct._id) - } else { - const { _id } = await shippingProfileService.retrieveDefault() - await shippingProfileService.addProduct(_id, newProduct._id) - } + let shippingProfile + // Get default shipping profile + if (value.is_giftcard) { + shippingProfile = await shippingProfileService.retrieveGiftCardDefault() + } else { + shippingProfile = await shippingProfileService.retrieveDefault() + } - newProduct = await productService.decorate( - newProduct, - [ - "title", - "description", - "tags", - "handle", - "images", - "thumbnail", - "options", - "published", - ], - ["variants"] - ) - res.json({ product: newProduct }) + newProduct = await productService + .withTransaction(manager) + .create({ ...value, profile_id: shippingProfile.id }) + + if (variants) { + const optionIds = value.options.map( + o => newProduct.options.find(newO => newO.title === o.title).id + ) + + await Promise.all( + variants.map(async v => { + const variant = { + ...v, + options: v.options.map((o, index) => ({ + ...o, + option_id: optionIds[index], + })), + } + await productVariantService + .withTransaction(manager) + .create(newProduct.id, variant) + }) + ) + } + }) + + const product = await productService.retrieve(newProduct.id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ product }) } catch (err) { - console.log(err) throw err } } diff --git a/packages/medusa/src/api/routes/admin/products/create-variant.js b/packages/medusa/src/api/routes/admin/products/create-variant.js index 80ade6f570..bf1e3a05ae 100644 --- a/packages/medusa/src/api/routes/admin/products/create-variant.js +++ b/packages/medusa/src/api/routes/admin/products/create-variant.js @@ -1,29 +1,47 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params + const schema = Validator.object().keys({ title: Validator.string().required(), - sku: Validator.string().optional(), - ean: Validator.string().optional(), + sku: Validator.string().allow(""), + ean: Validator.string().allow(""), + upc: Validator.string().allow(""), + barcode: Validator.string().allow(""), + hs_code: Validator.string().allow(""), + inventory_quantity: Validator.number().default(0), + allow_backorder: Validator.boolean().optional(), + manage_inventory: Validator.boolean().optional(), + weight: Validator.number().optional(), + length: Validator.number().optional(), + height: Validator.number().optional(), + width: Validator.number().optional(), + origin_country: Validator.string().allow(""), + mid_code: Validator.string().allow(""), + material: Validator.string().allow(""), + metadata: Validator.object().optional(), prices: Validator.array() - .items({ - currency_code: Validator.string().required(), - amount: Validator.number().required(), - sale_amount: Validator.number().optional(), - }) + .items( + Validator.object() + .keys({ + region_id: Validator.string(), + currency_code: Validator.string().required(), + amount: Validator.number() + .integer() + .required(), + sale_amount: Validator.number().optional(), + }) + .xor("region_id", "currency_code") + ) .required(), options: Validator.array() .items({ - option_id: Validator.objectId().required(), + option_id: Validator.string().required(), value: Validator.string().required(), }) .default([]), - image: Validator.string().optional(), - inventory_quantity: Validator.number().optional(), - allow_backorder: Validator.boolean().optional(), - manage_inventory: Validator.boolean().optional(), - metadata: Validator.object().optional(), }) const { value, error } = schema.validate(req.body) @@ -32,24 +50,17 @@ export default async (req, res) => { } try { + const productVariantService = req.scope.resolve("productVariantService") const productService = req.scope.resolve("productService") - const product = await productService.createVariant(id, value) - const data = await productService.decorate( - product, - [ - "title", - "description", - "is_giftcard", - "tags", - "thumbnail", - "handle", - "images", - "options", - "published", - ], - ["variants"] - ) - res.json({ product: data }) + + await productVariantService.create(id, value) + + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ product }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/delete-option.js b/packages/medusa/src/api/routes/admin/products/delete-option.js index 302d442ad3..e869a4ca96 100644 --- a/packages/medusa/src/api/routes/admin/products/delete-option.js +++ b/packages/medusa/src/api/routes/admin/products/delete-option.js @@ -1,23 +1,15 @@ +import { defaultRelations, defaultFields } from "." + export default async (req, res) => { const { id, option_id } = req.params try { const productService = req.scope.resolve("productService") - const product = await productService.deleteOption(id, option_id) - const data = await productService.decorate( - product, - [ - "title", - "description", - "tags", - "handle", - "thumbnail", - "images", - "options", - "published", - ], - ["variants"] - ) + await productService.deleteOption(id, option_id) + const data = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.json({ option_id, diff --git a/packages/medusa/src/api/routes/admin/products/delete-variant.js b/packages/medusa/src/api/routes/admin/products/delete-variant.js index 7345168354..c91cb978d9 100644 --- a/packages/medusa/src/api/routes/admin/products/delete-variant.js +++ b/packages/medusa/src/api/routes/admin/products/delete-variant.js @@ -1,23 +1,17 @@ +import { defaultRelations, defaultFields } from "." export default async (req, res) => { const { id, variant_id } = req.params try { + const productVariantService = req.scope.resolve("productVariantService") const productService = req.scope.resolve("productService") - const product = await productService.deleteVariant(id, variant_id) - const data = await productService.decorate( - product, - [ - "title", - "description", - "tags", - "handle", - "images", - "thumbnail", - "options", - "published", - ], - ["variants"] - ) + + await productVariantService.delete(variant_id) + + const data = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.json({ variant_id, diff --git a/packages/medusa/src/api/routes/admin/products/get-product.js b/packages/medusa/src/api/routes/admin/products/get-product.js index 4356933146..77daff520d 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/get-product.js @@ -1,24 +1,14 @@ +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { const { id } = req.params const productService = req.scope.resolve("productService") - let product = await productService.retrieve(id) - product = await productService.decorate( - product, - [ - "title", - "description", - "is_giftcard", - "tags", - "thumbnail", - "handle", - "images", - "options", - "published", - ], - ["variants"] - ) + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/index.js b/packages/medusa/src/api/routes/admin/products/index.js index 3004091a57..be70604d03 100644 --- a/packages/medusa/src/api/routes/admin/products/index.js +++ b/packages/medusa/src/api/routes/admin/products/index.js @@ -8,21 +8,12 @@ export default app => { route.post("/", middlewares.wrap(require("./create-product").default)) route.post("/:id", middlewares.wrap(require("./update-product").default)) - route.post( - "/:id/publish", - middlewares.wrap(require("./publish-product").default) - ) route.post( "/:id/variants", middlewares.wrap(require("./create-variant").default) ) - route.get( - "/:id/variants", - middlewares.wrap(require("./get-variants").default) - ) - route.post( "/:id/variants/:variant_id", middlewares.wrap(require("./update-variant").default) @@ -44,8 +35,74 @@ export default app => { middlewares.wrap(require("./delete-option").default) ) + route.post( + "/:id/metadata", + middlewares.wrap(require("./set-metadata").default) + ) + route.get("/:id", middlewares.wrap(require("./get-product").default)) route.get("/", middlewares.wrap(require("./list-products").default)) return app } + +export const defaultRelations = [ + "variants", + "variants.prices", + "variants.options", + "images", + "options", +] + +export const defaultFields = [ + "id", + "title", + "subtitle", + "description", + "tags", + "handle", + "is_giftcard", + "thumbnail", + "profile_id", + "weight", + "length", + "height", + "width", + "hs_code", + "origin_country", + "mid_code", + "material", + "created_at", + "updated_at", + "metadata", +] + +export const allowedFields = [ + "id", + "title", + "subtitle", + "description", + "tags", + "handle", + "is_giftcard", + "thumbnail", + "profile_id", + "weight", + "length", + "height", + "width", + "hs_code", + "origin_country", + "mid_code", + "material", + "created_at", + "updated_at", + "metadata", +] + +export const allowedRelations = [ + "variants", + "variants.prices", + "images", + "options", +] diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js index 0074b5d19a..e5190416fd 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -1,65 +1,33 @@ import _ from "lodash" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { try { - const variantService = req.scope.resolve("productVariantService") const productService = req.scope.resolve("productService") - const queryBuilderService = req.scope.resolve("queryBuilderService") - const query = queryBuilderService.buildQuery(req.query, [ - "title", - "description", - ]) - - if ("is_giftcard" in req.query) { - query.is_giftcard = req.query.is_giftcard === "true" - } - - let variantMatches = [] - if ("q" in req.query) { - let textQ = req.query.q - variantMatches = await variantService.list({ - $or: [ - { sku: new RegExp(textQ, "i") }, - { title: new RegExp(textQ, "i") }, - ], - }) - - query.$or = [ - ...query.$or, - { variants: { $in: variantMatches.map(({ _id }) => _id.toString()) } }, - ] - } - - const limit = parseInt(req.query.limit) || 0 + const limit = parseInt(req.query.limit) || 50 const offset = parseInt(req.query.offset) || 0 - let products = await productService.list(query, offset, limit) + const selector = {} - products = await Promise.all( - products.map( - async product => - await productService.decorate( - product, - [ - "title", - "description", - "is_giftcard", - "tags", - "thumbnail", - "handle", - "images", - "options", - "published", - ], - ["variants"] - ) - ) - ) + if ("q" in req.query) { + selector.q = req.query.q + } - const numProducts = await productService.count() + if ("is_giftcard" in req.query) { + selector.is_giftcard = req.query.is_giftcard === "true" + } - res.json({ products, total_count: numProducts }) + const listConfig = { + select: defaultFields, + relations: defaultRelations, + skip: offset, + take: limit, + } + + let products = await productService.list(selector, listConfig) + + res.json({ products, count: products.length, offset, limit }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/products/publish-product.js b/packages/medusa/src/api/routes/admin/products/publish-product.js deleted file mode 100644 index bf669ade46..0000000000 --- a/packages/medusa/src/api/routes/admin/products/publish-product.js +++ /dev/null @@ -1,27 +0,0 @@ -export default async (req, res) => { - const { id } = req.params - - try { - const productService = req.scope.resolve("productService") - const product = await productService.retrieve(id) - await productService.publish(product._id) - let publishedProduct = await productService.retrieve(product._id) - publishedProduct = await productService.decorate( - publishedProduct, - [ - "title", - "description", - "tags", - "handle", - "thumbnail", - "images", - "options", - "published", - ], - ["variants"] - ) - res.json({ product: publishedProduct }) - } catch (error) { - throw error - } -} diff --git a/packages/medusa/src/api/routes/admin/orders/set-metadata.js b/packages/medusa/src/api/routes/admin/products/set-metadata.js similarity index 54% rename from packages/medusa/src/api/routes/admin/orders/set-metadata.js rename to packages/medusa/src/api/routes/admin/products/set-metadata.js index c0ad8ed165..fbd509e1e7 100644 --- a/packages/medusa/src/api/routes/admin/orders/set-metadata.js +++ b/packages/medusa/src/api/routes/admin/products/set-metadata.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params @@ -14,15 +15,17 @@ export default async (req, res) => { } try { - const orderService = req.scope.resolve("orderService") - let order = await orderService.setMetadata(id, value.key, value.value) - order = await orderService.decorate( - order, - [], - ["region", "customer", "swaps"] - ) + const productService = req.scope.resolve("productService") + await productService.update(id, { + metadata: { [value.key]: value.value }, + }) - res.status(200).json({ order }) + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ product }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/update-option.js b/packages/medusa/src/api/routes/admin/products/update-option.js index 650f462b8c..232f31ea74 100644 --- a/packages/medusa/src/api/routes/admin/products/update-option.js +++ b/packages/medusa/src/api/routes/admin/products/update-option.js @@ -1,10 +1,11 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, option_id } = req.params const schema = Validator.object().keys({ - title: Validator.string(), + title: Validator.string().required(), }) const { value, error } = schema.validate(req.body) @@ -15,24 +16,14 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const product = await productService.updateOption(id, option_id, value) + await productService.updateOption(id, option_id, value) - const data = await productService.decorate( - product, - [ - "title", - "description", - "tags", - "handle", - "images", - "thumbnail", - "options", - "published", - ], - ["variants"] - ) + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - res.json({ product: data }) + res.json({ product }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js index 74e512de68..b8abd1dbcf 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.js +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -1,50 +1,64 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "." export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ - title: Validator.string(), + title: Validator.string().optional(), description: Validator.string().optional(), tags: Validator.string().optional(), - handle: Validator.string(), + handle: Validator.string().optional(), + weight: Validator.number().optional(), + length: Validator.number().optional(), + height: Validator.number().optional(), + width: Validator.number().optional(), + origin_country: Validator.string().allow(null, ""), + mid_code: Validator.string().allow(null, ""), + material: Validator.string().allow(null, ""), images: Validator.array() .items(Validator.string()) + .optional() .optional(), thumbnail: Validator.string().optional(), variants: Validator.array() .items({ - _id: Validator.string(), - title: Validator.string().optional(), - sku: Validator.string().optional(), - ean: Validator.string().optional(), - published: Validator.boolean(), - image: Validator.string() - .allow("") - .optional(), - barcode: Validator.string() - .allow("") - .optional(), + id: Validator.string().optional(), + title: Validator.string().allow(null), + sku: Validator.string().allow(null), + ean: Validator.string().allow(null), + barcode: Validator.string().allow(null), prices: Validator.array().items( Validator.object() .keys({ region_id: Validator.string(), currency_code: Validator.string(), - amount: Validator.number().required(), - sale_amount: Validator.number().optional(), + amount: Validator.number() + .integer() + .required(), }) .xor("region_id", "currency_code") ), options: Validator.array().items({ - option_id: Validator.objectId().required(), + option_id: Validator.string().required(), value: Validator.alternatives( Validator.string(), Validator.number() ).required(), }), - inventory_quantity: Validator.number().optional(), - allow_backorder: Validator.boolean().optional(), - manage_inventory: Validator.boolean().optional(), + inventory_quantity: Validator.number().allow(null), + allow_backorder: Validator.boolean().allow(null), + manage_inventory: Validator.boolean().allow(null), + weight: Validator.number().optional(), + length: Validator.number().optional(), + height: Validator.number().optional(), + width: Validator.number().optional(), + hs_code: Validator.string() + .optional() + .allow(null, ""), + origin_country: Validator.string().allow(null, ""), + mid_code: Validator.string().allow(null, ""), + material: Validator.string().allow(null, ""), metadata: Validator.object().optional(), }) .optional(), @@ -58,33 +72,15 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const oldProduct = await productService.retrieve(id) - if ( - !oldProduct.thumbnail && - !value.thumbnail && - value.images && - value.images.length - ) { - value.thumbnail = value.images[0] - } + await productService.update(id, value) - const product = await productService.update(oldProduct._id, value) - const data = await productService.decorate( - product, - [ - "title", - "description", - "tags", - "handle", - "images", - "thumbnail", - "options", - "published", - ], - ["variants"] - ) - res.json({ product: data }) + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ product }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.js b/packages/medusa/src/api/routes/admin/products/update-variant.js index 1b34c17faf..dbb7e41453 100644 --- a/packages/medusa/src/api/routes/admin/products/update-variant.js +++ b/packages/medusa/src/api/routes/admin/products/update-variant.js @@ -1,33 +1,52 @@ import _ from "lodash" import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, variant_id } = req.params const schema = Validator.object().keys({ title: Validator.string().optional(), - sku: Validator.string(), - ean: Validator.string(), + sku: Validator.string().optional(), + ean: Validator.string().optional(), + barcode: Validator.string().optional(), prices: Validator.array().items( Validator.object() .keys({ region_id: Validator.string(), currency_code: Validator.string(), - amount: Validator.number().required(), + amount: Validator.number() + .integer() + .required(), sale_amount: Validator.number().optional(), }) .xor("region_id", "currency_code") ), options: Validator.array().items({ - option_id: Validator.objectId().required(), + option_id: Validator.string().required(), value: Validator.alternatives( Validator.string(), Validator.number() ).required(), }), - image: Validator.string().optional(), inventory_quantity: Validator.number().optional(), allow_backorder: Validator.boolean().optional(), manage_inventory: Validator.boolean().optional(), + weight: Validator.number().optional(), + length: Validator.number().optional(), + height: Validator.number().optional(), + width: Validator.number().optional(), + hs_code: Validator.string() + .optional() + .allow(null, ""), + origin_country: Validator.string() + .optional() + .allow(null, ""), + mid_code: Validator.string() + .optional() + .allow(null, ""), + material: Validator.string() + .optional() + .allow(null, ""), metadata: Validator.object().optional(), }) @@ -40,74 +59,13 @@ export default async (req, res) => { const productService = req.scope.resolve("productService") const productVariantService = req.scope.resolve("productVariantService") - if (value.prices && value.prices.length) { - for (const price of value.prices) { - if (price.region_id) { - await productVariantService.setRegionPrice( - variant_id, - price.region_id, - price.amount, - price.sale_amount || undefined - ) - } else { - await productVariantService.setCurrencyPrice( - variant_id, - price.currency_code, - price.amount, - price.sale_amount || undefined - ) - } - } - } + await productVariantService.update(variant_id, value) - if (value.options && value.options.length) { - for (const option of value.options) { - await productService.updateOptionValue( - id, - variant_id, - option.option_id, - option.value - ) - } - } - - delete value.prices - delete value.options - - if (!_.isEmpty(value.metadata)) { - for (let key of Object.keys(value.metadata)) { - await productVariantService.setMetadata( - variant_id, - key, - value.metadata[key] - ) - } - - delete value.metadata - } - - if (!_.isEmpty(value)) { - await productVariantService.update(variant_id, value) - } - - const product = await productService.retrieve(id) - const data = await productService.decorate( - product, - [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "thumbnail", - "variants", - "is_giftcard", - "published", - ], - ["variants"] - ) - res.json({ product: data }) + const product = await productService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + res.json({ product }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js b/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js index 14d1ee9a43..4d0d8fc4cb 100644 --- a/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js +++ b/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js @@ -2,6 +2,24 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { RegionServiceMock } from "../../../../../services/__mocks__/region" +const defaultFields = [ + "id", + "name", + "currency_code", + "tax_rate", + "tax_code", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = [ + "countries", + "payment_providers", + "fulfillment_providers", +] + describe("GET /admin/regions/:region_id", () => { describe("successful creation", () => { let subject @@ -24,7 +42,11 @@ describe("GET /admin/regions/:region_id", () => { it("calls service addCountry", () => { expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(RegionServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("testRegion") + IdMap.getId("testRegion"), + { + select: defaultFields, + relations: defaultRelations, + } ) }) }) diff --git a/packages/medusa/src/api/routes/admin/regions/__tests__/list-regions.js b/packages/medusa/src/api/routes/admin/regions/__tests__/list-regions.js index 51236cc821..695a8261ee 100644 --- a/packages/medusa/src/api/routes/admin/regions/__tests__/list-regions.js +++ b/packages/medusa/src/api/routes/admin/regions/__tests__/list-regions.js @@ -2,6 +2,24 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { RegionServiceMock } from "../../../../../services/__mocks__/region" +const defaultFields = [ + "id", + "name", + "currency_code", + "tax_rate", + "tax_code", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = [ + "countries", + "payment_providers", + "fulfillment_providers", +] + describe("GET /admin/regions", () => { describe("successful creation", () => { let subject @@ -22,7 +40,15 @@ describe("GET /admin/regions", () => { it("calls service addCountry", () => { expect(RegionServiceMock.list).toHaveBeenCalledTimes(1) - expect(RegionServiceMock.list).toHaveBeenCalledWith({}) + expect(RegionServiceMock.list).toHaveBeenCalledWith( + {}, + { + select: defaultFields, + relations: defaultRelations, + take: 50, + skip: 0, + } + ) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/regions/add-country.js b/packages/medusa/src/api/routes/admin/regions/add-country.js index 902302eba3..e4d2caf6bf 100644 --- a/packages/medusa/src/api/routes/admin/regions/add-country.js +++ b/packages/medusa/src/api/routes/admin/regions/add-country.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id } = req.params @@ -15,8 +16,12 @@ export default async (req, res) => { const regionService = req.scope.resolve("regionService") await regionService.addCountry(region_id, value.country_code) - const data = await regionService.retrieve(region_id) - res.status(200).json({ region: data }) + const region = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ region }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/add-fulfillment-provider.js b/packages/medusa/src/api/routes/admin/regions/add-fulfillment-provider.js index aa56da1743..4d2529c281 100644 --- a/packages/medusa/src/api/routes/admin/regions/add-fulfillment-provider.js +++ b/packages/medusa/src/api/routes/admin/regions/add-fulfillment-provider.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id } = req.params @@ -15,7 +16,10 @@ export default async (req, res) => { const regionService = req.scope.resolve("regionService") await regionService.addFulfillmentProvider(region_id, value.provider_id) - const data = await regionService.retrieve(region_id) + const data = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ region: data }) } catch (err) { throw err diff --git a/packages/medusa/src/api/routes/admin/regions/add-payment-provider.js b/packages/medusa/src/api/routes/admin/regions/add-payment-provider.js index cda885ae63..b522a4ff32 100644 --- a/packages/medusa/src/api/routes/admin/regions/add-payment-provider.js +++ b/packages/medusa/src/api/routes/admin/regions/add-payment-provider.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id } = req.params @@ -15,7 +16,10 @@ export default async (req, res) => { const regionService = req.scope.resolve("regionService") await regionService.addPaymentProvider(region_id, value.provider_id) - const data = await regionService.retrieve(region_id) + const data = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ region: data }) } catch (err) { throw err diff --git a/packages/medusa/src/api/routes/admin/regions/create-region.js b/packages/medusa/src/api/routes/admin/regions/create-region.js index 78467c8b73..a1a9b6c8f5 100644 --- a/packages/medusa/src/api/routes/admin/regions/create-region.js +++ b/packages/medusa/src/api/routes/admin/regions/create-region.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const schema = Validator.object().keys({ @@ -18,8 +19,14 @@ export default async (req, res) => { try { const regionService = req.scope.resolve("regionService") - const data = await regionService.create(value) - res.status(200).json({ region: data }) + const result = await regionService.create(value) + + const region = await regionService.retrieve(result.id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ region }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/delete-metadata.js b/packages/medusa/src/api/routes/admin/regions/delete-metadata.js index 120df0e9c9..87fd62cbd8 100644 --- a/packages/medusa/src/api/routes/admin/regions/delete-metadata.js +++ b/packages/medusa/src/api/routes/admin/regions/delete-metadata.js @@ -1,9 +1,16 @@ +import { defaultRelations, defaultFields } from "./" + export default async (req, res) => { const { id, key } = req.params try { const regionService = req.scope.resolve("regionService") - const region = await regionService.deleteMetadata(id, key) + await regionService.deleteMetadata(id, key) + + const region = await regionService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ region }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/regions/get-fulfillment-options.js b/packages/medusa/src/api/routes/admin/regions/get-fulfillment-options.js index a590e48791..9dba5d8b67 100644 --- a/packages/medusa/src/api/routes/admin/regions/get-fulfillment-options.js +++ b/packages/medusa/src/api/routes/admin/regions/get-fulfillment-options.js @@ -1,16 +1,19 @@ -import { MedusaError, Validator } from "medusa-core-utils" - export default async (req, res) => { const { region_id } = req.params + try { const fulfillmentProviderService = req.scope.resolve( "fulfillmentProviderService" ) const regionService = req.scope.resolve("regionService") - const region = await regionService.retrieve(region_id) + const region = await regionService.retrieve(region_id, { + relations: ["fulfillment_providers"], + }) + + const fpsIds = region.fulfillment_providers.map(fp => fp.id) || [] const options = await fulfillmentProviderService.listFulfillmentOptions( - region.fulfillment_providers || [] + fpsIds ) res.status(200).json({ diff --git a/packages/medusa/src/api/routes/admin/regions/get-region.js b/packages/medusa/src/api/routes/admin/regions/get-region.js index 184078ee8e..56ee0c1f9c 100644 --- a/packages/medusa/src/api/routes/admin/regions/get-region.js +++ b/packages/medusa/src/api/routes/admin/regions/get-region.js @@ -1,10 +1,14 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id } = req.params try { const regionService = req.scope.resolve("regionService") - const data = await regionService.retrieve(region_id) + const data = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ region: data }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/regions/index.js b/packages/medusa/src/api/routes/admin/regions/index.js index 4618e25b88..0aab12bacf 100644 --- a/packages/medusa/src/api/routes/admin/regions/index.js +++ b/packages/medusa/src/api/routes/admin/regions/index.js @@ -70,3 +70,21 @@ export default app => { return app } + +export const defaultFields = [ + "id", + "name", + "currency_code", + "tax_rate", + "tax_code", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const defaultRelations = [ + "countries", + "payment_providers", + "fulfillment_providers", +] diff --git a/packages/medusa/src/api/routes/admin/regions/list-regions.js b/packages/medusa/src/api/routes/admin/regions/list-regions.js index fae0d13772..b09de83221 100644 --- a/packages/medusa/src/api/routes/admin/regions/list-regions.js +++ b/packages/medusa/src/api/routes/admin/regions/list-regions.js @@ -1,11 +1,24 @@ -import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { try { const regionService = req.scope.resolve("regionService") - const data = await regionService.list({}) - res.status(200).json({ regions: data }) + const limit = parseInt(req.query.limit) || 50 + const offset = parseInt(req.query.offset) || 0 + + const selector = {} + + const listConfig = { + select: defaultFields, + relations: defaultRelations, + skip: offset, + take: limit, + } + + let regions = await regionService.list(selector, listConfig) + + res.json({ regions, count: regions.length, offset, limit }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/remove-country.js b/packages/medusa/src/api/routes/admin/regions/remove-country.js index b8585198ec..b56ff7b362 100644 --- a/packages/medusa/src/api/routes/admin/regions/remove-country.js +++ b/packages/medusa/src/api/routes/admin/regions/remove-country.js @@ -1,12 +1,18 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id, country_code } = req.params try { const regionService = req.scope.resolve("regionService") - const data = await regionService.removeCountry(region_id, country_code) + await regionService.removeCountry(region_id, country_code) - res.json({ region: data }) + const region = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ region }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/remove-fulfillment-provider.js b/packages/medusa/src/api/routes/admin/regions/remove-fulfillment-provider.js index 0b0b2eb8a1..19db7d94f0 100644 --- a/packages/medusa/src/api/routes/admin/regions/remove-fulfillment-provider.js +++ b/packages/medusa/src/api/routes/admin/regions/remove-fulfillment-provider.js @@ -1,15 +1,19 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id, provider_id } = req.params try { const regionService = req.scope.resolve("regionService") - const data = await regionService.removeFulfillmentProvider( - region_id, - provider_id - ) - res.json({ region: data }) + await regionService.removeFulfillmentProvider(region_id, provider_id) + + const region = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ region }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/remove-payment-provider.js b/packages/medusa/src/api/routes/admin/regions/remove-payment-provider.js index 173b1b137e..906cefceab 100644 --- a/packages/medusa/src/api/routes/admin/regions/remove-payment-provider.js +++ b/packages/medusa/src/api/routes/admin/regions/remove-payment-provider.js @@ -1,15 +1,18 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id, provider_id } = req.params try { const regionService = req.scope.resolve("regionService") - const data = await regionService.removePaymentProvider( - region_id, - provider_id - ) + await regionService.removePaymentProvider(region_id, provider_id) - res.json({ region: data }) + const region = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ region }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/regions/set-metadata.js b/packages/medusa/src/api/routes/admin/regions/set-metadata.js index 2ab68d12ba..36445b2dd5 100644 --- a/packages/medusa/src/api/routes/admin/regions/set-metadata.js +++ b/packages/medusa/src/api/routes/admin/regions/set-metadata.js @@ -15,7 +15,12 @@ export default async (req, res) => { try { const regionService = req.scope.resolve("regionService") - const region = await regionService.setMetadata(id, value.key, value.value) + await regionService.setMetadata(id, value.key, value.value) + + const region = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ region }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/regions/update-region.js b/packages/medusa/src/api/routes/admin/regions/update-region.js index e946882bed..0199db5167 100644 --- a/packages/medusa/src/api/routes/admin/regions/update-region.js +++ b/packages/medusa/src/api/routes/admin/regions/update-region.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultRelations, defaultFields } from "./" export default async (req, res) => { const { region_id } = req.params @@ -9,6 +10,7 @@ export default async (req, res) => { tax_rate: Validator.number(), payment_providers: Validator.array().items(Validator.string()), fulfillment_providers: Validator.array().items(Validator.string()), + // iso_2 country codes countries: Validator.array().items(Validator.string()), }) @@ -19,8 +21,13 @@ export default async (req, res) => { try { const regionService = req.scope.resolve("regionService") - const data = await regionService.update(region_id, value) - res.status(200).json({ region: data }) + await regionService.update(region_id, value) + const region = await regionService.retrieve(region_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ region }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/returns/index.js b/packages/medusa/src/api/routes/admin/returns/index.js new file mode 100644 index 0000000000..0c9ade22c2 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/returns/index.js @@ -0,0 +1,15 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/returns", route) + + /** + * List returns + */ + route.get("/", middlewares.wrap(require("./list-returns").default)) + + return app +} diff --git a/packages/medusa/src/api/routes/admin/returns/list-returns.js b/packages/medusa/src/api/routes/admin/returns/list-returns.js new file mode 100644 index 0000000000..65cd1f99ed --- /dev/null +++ b/packages/medusa/src/api/routes/admin/returns/list-returns.js @@ -0,0 +1,25 @@ +import _ from "lodash" + +export default async (req, res) => { + try { + const returnService = req.scope.resolve("returnService") + + const limit = parseInt(req.query.limit) || 50 + const offset = parseInt(req.query.offset) || 0 + + const selector = {} + + const listConfig = { + relations: ["swap", "order"], + skip: offset, + take: limit, + order: { created_at: "DESC" }, + } + + const returns = await returnService.list(selector, { ...listConfig }) + + res.json({ returns, count: returns.length, offset, limit }) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/shipping-options/__tests__/create-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/__tests__/create-shipping-option.js index ffcfe15e74..f8a8a0e378 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/__tests__/create-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/__tests__/create-shipping-option.js @@ -14,14 +14,12 @@ describe("POST /admin/shipping-options", () => { region_id: "testregion", provider_id: "test_provider", data: { id: "test" }, - price: { - type: "flat_rate", - amount: 100, - }, + price_type: "flat_rate", + amount: 100, requirements: [ { type: "min_subtotal", - value: 1, + amount: 1, }, ], }, @@ -46,14 +44,12 @@ describe("POST /admin/shipping-options", () => { provider_id: "test_provider", data: { id: "test" }, profile_id: expect.stringMatching(/.*/), - price: { - type: "flat_rate", - amount: 100, - }, + price_type: "flat_rate", + amount: 100, requirements: [ { type: "min_subtotal", - value: 1, + amount: 1, }, ], }) @@ -67,14 +63,12 @@ describe("POST /admin/shipping-options", () => { jest.clearAllMocks() subject = await request("POST", "/admin/shipping-options", { payload: { - price: { - type: "flat_rate", - amount: 100, - }, + price_type: "flat_rate", + amount: 100, requirements: [ { type: "min_subtotal", - value: 1, + amount: 1, }, ], }, diff --git a/packages/medusa/src/api/routes/admin/shipping-options/__tests__/list-shipping-options.js b/packages/medusa/src/api/routes/admin/shipping-options/__tests__/list-shipping-options.js index 86005d55a9..65d645f0da 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/__tests__/list-shipping-options.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/__tests__/list-shipping-options.js @@ -2,6 +2,24 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { ShippingOptionServiceMock } from "../../../../../services/__mocks__/shipping-option" +const defaultFields = [ + "id", + "name", + "region_id", + "profile_id", + "provider_id", + "price_type", + "amount", + "is_return", + "data", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = ["region", "profile", "requirements"] + describe("GET /admin/shipping-options", () => { describe("successful retrieval", () => { let subject @@ -22,7 +40,13 @@ describe("GET /admin/shipping-options", () => { it("calls service retrieve", () => { expect(ShippingOptionServiceMock.list).toHaveBeenCalledTimes(1) - expect(ShippingOptionServiceMock.list).toHaveBeenCalledWith({}) + expect(ShippingOptionServiceMock.list).toHaveBeenCalledWith( + {}, + { + select: defaultFields, + relations: defaultRelations, + } + ) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/shipping-options/__tests__/update-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/__tests__/update-shipping-option.js index 1efdce6317..f6886a05f1 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/__tests__/update-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/__tests__/update-shipping-option.js @@ -13,14 +13,12 @@ describe("POST /admin/shipping-options", () => { { payload: { name: "Test option", - price: { - type: "flat_rate", - amount: 100, - }, + amount: 100, requirements: [ { + id: "yes", type: "min_subtotal", - value: 1, + amount: 1, }, ], }, @@ -43,14 +41,12 @@ describe("POST /admin/shipping-options", () => { IdMap.getId("validId"), { name: "Test option", - price: { - type: "flat_rate", - amount: 100, - }, + amount: 100, requirements: [ { + id: "yes", type: "min_subtotal", - value: 1, + amount: 1, }, ], } diff --git a/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js index a5b3f7efba..dc1034a6ce 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const schema = Validator.object().keys({ @@ -7,17 +8,17 @@ export default async (req, res) => { provider_id: Validator.string().required(), profile_id: Validator.string(), data: Validator.object().required(), - price: Validator.object() - .keys({ - type: Validator.string().required(), - amount: Validator.number().optional(), - }) - .required(), + price_type: Validator.string().required(), + amount: Validator.number() + .integer() + .optional(), requirements: Validator.array() .items( Validator.object({ type: Validator.string().required(), - value: Validator.number().required(), + amount: Validator.number() + .integer() + .required(), }) ) .optional(), @@ -35,13 +36,15 @@ export default async (req, res) => { // Add to default shipping profile if (!value.profile_id) { - const { _id } = await shippingProfileService.retrieveDefault() - value.profile_id = _id + const { id } = await shippingProfileService.retrieveDefault() + value.profile_id = id } - const data = await optionService.create(value) - - await shippingProfileService.addShippingOption(value.profile_id, data._id) + const result = await optionService.create(value) + const data = await optionService.retrieve(result.id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ shipping_option: data }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/shipping-options/delete-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/delete-shipping-option.js index 8a2385057d..3be84f4e18 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/delete-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/delete-shipping-option.js @@ -1,5 +1,3 @@ -import { MedusaError, Validator } from "medusa-core-utils" - export default async (req, res) => { const { option_id } = req.params try { diff --git a/packages/medusa/src/api/routes/admin/shipping-options/index.js b/packages/medusa/src/api/routes/admin/shipping-options/index.js index 5d590692a2..b51632da78 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/index.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/index.js @@ -24,3 +24,21 @@ export default app => { return app } + +export const defaultFields = [ + "id", + "name", + "region_id", + "profile_id", + "provider_id", + "price_type", + "amount", + "is_return", + "data", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const defaultRelations = ["region", "profile", "requirements"] diff --git a/packages/medusa/src/api/routes/admin/shipping-options/list-shipping-options.js b/packages/medusa/src/api/routes/admin/shipping-options/list-shipping-options.js index fdaafe1baf..e6f6bbabfa 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/list-shipping-options.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/list-shipping-options.js @@ -1,11 +1,15 @@ import _ from "lodash" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { try { - const query = _.pick(req.query, ["region_id", "region_id[]", "is_return"]) + const query = _.pick(req.query, ["region_id", "is_return"]) const optionService = req.scope.resolve("shippingOptionService") - const data = await optionService.list(query) + const data = await optionService.list(query, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ shipping_options: data }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/shipping-options/update-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/update-shipping-option.js index 919d63ca73..d64e6202c4 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/update-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/update-shipping-option.js @@ -1,21 +1,22 @@ import _ from "lodash" import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { option_id } = req.params const schema = Validator.object().keys({ name: Validator.string().optional(), - price: Validator.object() - .keys({ - type: Validator.string().required(), - amount: Validator.number().optional(), - }) + amount: Validator.number() + .integer() .optional(), requirements: Validator.array() .items( Validator.object({ + id: Validator.string().required(), type: Validator.string().required(), - value: Validator.number().required(), + amount: Validator.number() + .integer() + .required(), }) ) .optional(), @@ -29,15 +30,13 @@ export default async (req, res) => { try { const optionService = req.scope.resolve("shippingOptionService") - if (!_.isEmpty(value.metadata)) { - for (let key of Object.keys(value.metadata)) { - await optionService.setMetadata(option_id, key, value.metadata[key]) - } - delete value.metadata - } + await optionService.update(option_id, value) - const data = await optionService.update(option_id, value) + const data = await optionService.retrieve(option_id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ shipping_option: data }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-product.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-product.js deleted file mode 100644 index 58cf4a3851..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-product.js +++ /dev/null @@ -1,39 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" - -describe("POST /admin/shipping-profiles/:profile_id/products", () => { - describe("successful addition", () => { - let subject - - beforeAll(async () => { - const profileId = IdMap.getId("validId") - subject = await request( - "POST", - `/admin/shipping-profiles/${profileId}/products`, - { - payload: { - product_id: IdMap.getId("validId"), - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service retrieve", () => { - expect(ShippingProfileServiceMock.addProduct).toHaveBeenCalledTimes(1) - expect(ShippingProfileServiceMock.addProduct).toHaveBeenCalledWith( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-shipping-option.js deleted file mode 100644 index 334827e511..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/add-shipping-option.js +++ /dev/null @@ -1,41 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" - -describe("POST /admin/shipping-profiles/:profile_id/products", () => { - describe("successful addition", () => { - let subject - - beforeAll(async () => { - const profileId = IdMap.getId("validId") - subject = await request( - "POST", - `/admin/shipping-profiles/${profileId}/shipping-options`, - { - payload: { - option_id: IdMap.getId("validId"), - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service retrieve", () => { - expect( - ShippingProfileServiceMock.addShippingOption - ).toHaveBeenCalledTimes(1) - expect(ShippingProfileServiceMock.addShippingOption).toHaveBeenCalledWith( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js index bee5890c99..2074baa787 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js @@ -2,6 +2,18 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" +const defaultFields = [ + "id", + "name", + "type", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +const defaultRelations = ["products", "shipping_options"] + describe("GET /admin/shipping-profiles/:profile_id", () => { describe("successful retrieval", () => { let subject @@ -27,7 +39,11 @@ describe("GET /admin/shipping-profiles/:profile_id", () => { it("calls service retrieve", () => { expect(ShippingProfileServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(ShippingProfileServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("validId") + IdMap.getId("validId"), + { + select: defaultFields, + relations: defaultRelations, + } ) }) }) diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-product.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-product.js deleted file mode 100644 index 8afd11fce4..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-product.js +++ /dev/null @@ -1,37 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" - -describe("DELETE /admin/shipping-profiles/:profile_id/products/:product_id", () => { - describe("successful addition", () => { - let subject - - beforeAll(async () => { - const profileId = IdMap.getId("validId") - const productId = IdMap.getId("validId") - subject = await request( - "DELETE", - `/admin/shipping-profiles/${profileId}/products/${productId}`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service retrieve", () => { - expect(ShippingProfileServiceMock.removeProduct).toHaveBeenCalledTimes(1) - expect(ShippingProfileServiceMock.removeProduct).toHaveBeenCalledWith( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-shipping-option.js deleted file mode 100644 index e8abaca892..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/remove-shipping-option.js +++ /dev/null @@ -1,38 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" - -describe("DELETE /admin/shipping-profiles/:profile_id/shipping-options/:option_id", () => { - describe("successful addition", () => { - let subject - - beforeAll(async () => { - const profileId = IdMap.getId("validId") - const optionId = IdMap.getId("validId") - subject = await request( - "DELETE", - `/admin/shipping-profiles/${profileId}/shipping-options/${optionId}`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service retrieve", () => { - expect( - ShippingProfileServiceMock.removeShippingOption - ).toHaveBeenCalledTimes(1) - expect( - ShippingProfileServiceMock.removeShippingOption - ).toHaveBeenCalledWith(IdMap.getId("validId"), IdMap.getId("validId")) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/update-shipping-profile.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/update-shipping-profile.js index a8552d2bf3..ae744b1e04 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/update-shipping-profile.js +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/update-shipping-profile.js @@ -13,8 +13,6 @@ describe("POST /admin/shipping-profile", () => { { payload: { name: "Test option", - products: [IdMap.getId("product1")], - shipping_options: [IdMap.getId("shipping1")], }, adminSession: { jwt: { @@ -35,8 +33,6 @@ describe("POST /admin/shipping-profile", () => { IdMap.getId("validId"), { name: "Test option", - products: [IdMap.getId("product1")], - shipping_options: [IdMap.getId("shipping1")], } ) }) diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/add-product.js b/packages/medusa/src/api/routes/admin/shipping-profiles/add-product.js deleted file mode 100644 index db59ac30de..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/add-product.js +++ /dev/null @@ -1,21 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { profile_id } = req.params - const schema = Validator.object().keys({ - product_id: Validator.objectId().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const profileService = req.scope.resolve("shippingProfileService") - const data = await profileService.addProduct(profile_id, value.product_id) - res.status(200).json({ shipping_profile: data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/add-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-profiles/add-shipping-option.js deleted file mode 100644 index 41b3264f47..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/add-shipping-option.js +++ /dev/null @@ -1,24 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { profile_id } = req.params - const schema = Validator.object().keys({ - option_id: Validator.objectId().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const profileService = req.scope.resolve("shippingProfileService") - const data = await profileService.addShippingOption( - profile_id, - value.option_id - ) - res.status(200).json({ shipping_profile: data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/get-shipping-profile.js b/packages/medusa/src/api/routes/admin/shipping-profiles/get-shipping-profile.js index 9c0adad813..ce7381b49e 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/get-shipping-profile.js +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/get-shipping-profile.js @@ -1,15 +1,13 @@ +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { profile_id } = req.params try { const profileService = req.scope.resolve("shippingProfileService") - const data = await profileService.retrieve(profile_id) - - const profile = await profileService.decorate( - data, - ["name"], - ["products", "shipping_options"] - ) + const profile = await profileService.retrieve(profile_id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ shipping_profile: profile }) } catch (err) { diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/index.js b/packages/medusa/src/api/routes/admin/shipping-profiles/index.js index 2c1f7c14f8..aaf2b7901d 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/index.js +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/index.js @@ -25,25 +25,17 @@ export default app => { middlewares.wrap(require("./delete-shipping-profile").default) ) - // Product management - route.post( - "/:profile_id/products", - middlewares.wrap(require("./add-product").default) - ) - route.delete( - "/:profile_id/products/:product_id", - middlewares.wrap(require("./remove-product").default) - ) - - // Shipping Option management - route.post( - "/:profile_id/shipping-options", - middlewares.wrap(require("./add-shipping-option").default) - ) - route.delete( - "/:profile_id/shipping-options/:option_id", - middlewares.wrap(require("./remove-shipping-option").default) - ) - return app } + +export const defaultFields = [ + "id", + "name", + "type", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const defaultRelations = ["products", "shipping_options"] diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/remove-product.js b/packages/medusa/src/api/routes/admin/shipping-profiles/remove-product.js deleted file mode 100644 index 29d93115fe..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/remove-product.js +++ /dev/null @@ -1,14 +0,0 @@ -export default async (req, res) => { - const { profile_id, product_id } = req.params - - try { - const profileService = req.scope.resolve("shippingProfileService") - - await profileService.removeProduct(profile_id, product_id) - - const data = profileService.retrieve(profile_id) - res.status(200).json({ shipping_profile: data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/remove-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-profiles/remove-shipping-option.js deleted file mode 100644 index 2cc8107b45..0000000000 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/remove-shipping-option.js +++ /dev/null @@ -1,14 +0,0 @@ -export default async (req, res) => { - const { profile_id, option_id } = req.params - - try { - const profileService = req.scope.resolve("shippingProfileService") - - await profileService.removeShippingOption(profile_id, option_id) - - const data = profileService.retrieve(profile_id) - res.status(200).json({ shipping_profile: data }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/update-shipping-profile.js b/packages/medusa/src/api/routes/admin/shipping-profiles/update-shipping-profile.js index a93b497fbc..ad5fc346c6 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/update-shipping-profile.js +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/update-shipping-profile.js @@ -5,8 +5,6 @@ export default async (req, res) => { const schema = Validator.object().keys({ name: Validator.string(), - products: Validator.array().items(Validator.objectId()), - shipping_options: Validator.array().items(Validator.objectId()), }) const { value, error } = schema.validate(req.body) diff --git a/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js b/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js index 45348ad475..b2db40b1e7 100644 --- a/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js +++ b/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js @@ -22,7 +22,10 @@ describe("GET /admin/store", () => { it("calls service retrieve", () => { expect(StoreServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(StoreServiceMock.retrieve).toHaveBeenCalledWith() + expect(StoreServiceMock.retrieve).toHaveBeenCalledWith([ + "currencies", + "default_currency", + ]) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/store/get-store.js b/packages/medusa/src/api/routes/admin/store/get-store.js index 909443b444..1b7b0cc766 100644 --- a/packages/medusa/src/api/routes/admin/store/get-store.js +++ b/packages/medusa/src/api/routes/admin/store/get-store.js @@ -1,9 +1,21 @@ export default async (req, res) => { try { const storeService = req.scope.resolve("storeService") - const data = await storeService.retrieve() + const paymentProviderService = req.scope.resolve("paymentProviderService") + const fulfillmentProviderService = req.scope.resolve( + "fulfillmentProviderService" + ) + + const data = await storeService.retrieve(["currencies", "default_currency"]) + const paymentProviders = await paymentProviderService.list() + const fulfillmentProviders = await fulfillmentProviderService.list() + + data.payment_providers = paymentProviders + data.fulfillment_providers = fulfillmentProviders + res.status(200).json({ store: data }) } catch (err) { + console.log(err) throw err } } diff --git a/packages/medusa/src/api/routes/admin/store/index.js b/packages/medusa/src/api/routes/admin/store/index.js index 2bd8039bd7..db6f71c3ac 100644 --- a/packages/medusa/src/api/routes/admin/store/index.js +++ b/packages/medusa/src/api/routes/admin/store/index.js @@ -7,6 +7,10 @@ export default app => { app.use("/store", route) route.get("/", middlewares.wrap(require("./get-store").default)) + route.get( + "/payment-providers", + middlewares.wrap(require("./list-payment-providers").default) + ) route.post("/", middlewares.wrap(require("./update-store").default)) route.post( "/currencies/:currency_code", diff --git a/packages/medusa/src/api/routes/admin/store/list-payment-providers.js b/packages/medusa/src/api/routes/admin/store/list-payment-providers.js new file mode 100644 index 0000000000..3f329b49b6 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/list-payment-providers.js @@ -0,0 +1,9 @@ +export default async (req, res) => { + try { + const paymentProviderService = container.resolve("paymentProviderService") + const paymentProviders = await paymentProviderService.list() + res.status(200).json({ payment_providers: paymentProviders }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/store/update-store.js b/packages/medusa/src/api/routes/admin/store/update-store.js index 80a2d9da06..ce63b7fbc9 100644 --- a/packages/medusa/src/api/routes/admin/store/update-store.js +++ b/packages/medusa/src/api/routes/admin/store/update-store.js @@ -4,7 +4,7 @@ export default async (req, res) => { const schema = Validator.object().keys({ name: Validator.string(), swap_link_template: Validator.string(), - default_currency: Validator.string(), + default_currency_code: Validator.string(), currencies: Validator.array().items(Validator.string()), }) @@ -15,8 +15,8 @@ export default async (req, res) => { try { const storeService = req.scope.resolve("storeService") - const data = await storeService.update(value) - res.status(200).json({ store: data }) + const store = await storeService.update(value) + res.status(200).json({ store }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/swaps/get-swap.js b/packages/medusa/src/api/routes/admin/swaps/get-swap.js new file mode 100644 index 0000000000..114a641a69 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/swaps/get-swap.js @@ -0,0 +1,24 @@ +export default async (req, res) => { + const { id } = req.params + + try { + const orderService = req.scope.resolve("orderService") + + const order = await orderService.retrieve(id, { + relations: [ + "order", + "additional_items", + "return_order", + "fulfillments", + "payment", + "shipping_address", + "shipping_methods", + "cart", + ], + }) + + res.json({ order }) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/swaps/index.js b/packages/medusa/src/api/routes/admin/swaps/index.js new file mode 100644 index 0000000000..541bff2f10 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/swaps/index.js @@ -0,0 +1,20 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/swaps", route) + + /** + * List swaps + */ + route.get("/", middlewares.wrap(require("./list-swaps").default)) + + /** + * Get a swap + */ + route.get("/:id", middlewares.wrap(require("./get-swap").default)) + + return app +} diff --git a/packages/medusa/src/api/routes/admin/swaps/list-swaps.js b/packages/medusa/src/api/routes/admin/swaps/list-swaps.js new file mode 100644 index 0000000000..e5d5859faa --- /dev/null +++ b/packages/medusa/src/api/routes/admin/swaps/list-swaps.js @@ -0,0 +1,24 @@ +import _ from "lodash" + +export default async (req, res) => { + try { + const swapService = req.scope.resolve("swapService") + + const limit = parseInt(req.query.limit) || 50 + const offset = parseInt(req.query.offset) || 0 + + const selector = {} + + const listConfig = { + skip: offset, + take: limit, + order: { created_at: "DESC" }, + } + + const swaps = await swapService.list(selector, { ...listConfig }) + + res.json({ swaps, count: swaps.length, offset, limit }) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/set-password.js b/packages/medusa/src/api/routes/admin/users/__tests__/set-password.js deleted file mode 100644 index 90d94f89a5..0000000000 --- a/packages/medusa/src/api/routes/admin/users/__tests__/set-password.js +++ /dev/null @@ -1,46 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { UserServiceMock } from "../../../../../services/__mocks__/user" - -describe("POST /admin/users/:id/set-password", () => { - describe("successfully sets password", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/users/${IdMap.getId("test-user")}/set-password`, - { - payload: { - password: "987654321", - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls UserService setPassword", () => { - expect(UserServiceMock.setPassword).toHaveBeenCalledTimes(1) - expect(UserServiceMock.setPassword).toHaveBeenCalledWith( - IdMap.getId("test-user"), - "987654321" - ) - }) - - it("returns the user", () => { - expect(subject.body.user._id).toEqual(IdMap.getId("test-user")) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/users/index.js b/packages/medusa/src/api/routes/admin/users/index.js index 03e4db1e77..ee77035715 100644 --- a/packages/medusa/src/api/routes/admin/users/index.js +++ b/packages/medusa/src/api/routes/admin/users/index.js @@ -20,10 +20,6 @@ export default app => { middlewares.wrap(require("./reset-password").default) ) route.post("/:user_id", middlewares.wrap(require("./update-user").default)) - route.post( - "/:user_id/set-password", - middlewares.wrap(require("./set-password").default) - ) route.delete("/:user_id", middlewares.wrap(require("./delete-user").default)) diff --git a/packages/medusa/src/api/routes/admin/users/set-password.js b/packages/medusa/src/api/routes/admin/users/set-password.js deleted file mode 100644 index beee27d33c..0000000000 --- a/packages/medusa/src/api/routes/admin/users/set-password.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Validator, MedusaError } from "medusa-core-utils" - -export default async (req, res) => { - const { user_id } = req.params - const schema = Validator.object().keys({ - password: Validator.string().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const userService = req.scope.resolve("userService") - const data = await userService.setPassword(user_id, value.password) - res.json({ user: data }) - } catch (error) { - throw error - } -} diff --git a/packages/medusa/src/api/routes/admin/variants/index.js b/packages/medusa/src/api/routes/admin/variants/index.js new file mode 100644 index 0000000000..3287a1c7a6 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/variants/index.js @@ -0,0 +1,62 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/variants", route) + + route.get("/", middlewares.wrap(require("./list-variants").default)) + + return app +} + +export const defaultRelations = ["product", "prices", "options"] + +export const defaultFields = [ + "id", + "title", + "product_id", + "sku", + "barcode", + "ean", + "upc", + "inventory_quantity", + "allow_backorder", + "weight", + "length", + "height", + "width", + "hs_code", + "origin_country", + "mid_code", + "material", + "created_at", + "updated_at", + "metadata", +] + +export const allowedFields = [ + "id", + "title", + "product_id", + "sku", + "barcode", + "ean", + "upc", + "inventory_quantity", + "allow_backorder", + "weight", + "length", + "height", + "width", + "hs_code", + "origin_country", + "mid_code", + "material", + "created_at", + "updated_at", + "metadata", +] + +export const allowedRelations = ["product", "prices", "options"] diff --git a/packages/medusa/src/api/routes/admin/variants/list-variants.js b/packages/medusa/src/api/routes/admin/variants/list-variants.js new file mode 100644 index 0000000000..a159c593cc --- /dev/null +++ b/packages/medusa/src/api/routes/admin/variants/list-variants.js @@ -0,0 +1,30 @@ +import _ from "lodash" +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + try { + const variantService = req.scope.resolve("productVariantService") + + const limit = parseInt(req.query.limit) || 20 + const offset = parseInt(req.query.offset) || 0 + + const selector = {} + + if ("q" in req.query) { + selector.q = req.query.q + } + + const listConfig = { + select: defaultFields, + relations: defaultRelations, + skip: offset, + take: limit, + } + + let variants = await variantService.list(selector, listConfig) + + res.json({ variants, count: variants.length, offset, limit }) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/store/auth/create-session.js b/packages/medusa/src/api/routes/store/auth/create-session.js index 836a9867d6..5134d6049e 100644 --- a/packages/medusa/src/api/routes/store/auth/create-session.js +++ b/packages/medusa/src/api/routes/store/auth/create-session.js @@ -35,11 +35,10 @@ export default async (req, res) => { } ) - const data = await customerService.decorate( - result.customer, - ["_id", "email", "orders", "shipping_addresses", "first_name", "last_name"], - ["orders"] - ) + const customer = await customerService.retrieve(result.customer.id, [ + "orders", + "orders.items", + ]) - res.json({ customer: data }) + res.json({ customer }) } diff --git a/packages/medusa/src/api/routes/store/auth/get-session.js b/packages/medusa/src/api/routes/store/auth/get-session.js index cd74005668..6aeac8b90e 100644 --- a/packages/medusa/src/api/routes/store/auth/get-session.js +++ b/packages/medusa/src/api/routes/store/auth/get-session.js @@ -2,21 +2,12 @@ export default async (req, res) => { try { if (req.user && req.user.customer_id) { const customerService = req.scope.resolve("customerService") - const customer = await customerService.retrieve(req.user.customer_id) - - const data = await customerService.decorate( - customer, - [ - "_id", - "email", - "orders", - "shipping_addresses", - "first_name", - "last_name", - ], - ["orders"] - ) - res.json({ customer: data }) + const customer = await customerService.retrieve(req.user.customer_id, [ + "shipping_addresses", + "orders", + "orders.items", + ]) + res.json({ customer }) } else { res.sendStatus(401) } diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/add-shipping-method.js b/packages/medusa/src/api/routes/store/carts/__tests__/add-shipping-method.js index 5348ca2eae..93f418dcc6 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/add-shipping-method.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/add-shipping-method.js @@ -32,13 +32,16 @@ describe("POST /store/carts/:id/shipping-methods", () => { ) }) + it("calls CartService retrieve", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2) + }) + it("returns 200", () => { expect(subject.status).toEqual(200) }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("fr-cart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("fr-cart")) }) }) @@ -76,13 +79,16 @@ describe("POST /store/carts/:id/shipping-methods", () => { ) }) + it("calls CartService retrieve", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2) + }) + it("returns 200", () => { expect(subject.status).toEqual(200) }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("fr-cart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("fr-cart")) }) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js new file mode 100644 index 0000000000..489ecc6c4f --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js @@ -0,0 +1,120 @@ +import { IdMap } from "medusa-test-utils" +import { defaultFields, defaultRelations } from ".." +import { request } from "../../../../../helpers/test-request" +import { CartServiceMock } from "../../../../../services/__mocks__/cart" +import { OrderServiceMock } from "../../../../../services/__mocks__/order" +import { SwapServiceMock } from "../../../../../services/__mocks__/swap" + +describe("POST /store/carts/:id", () => { + describe("successfully completes a normal cart", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/store/carts/${IdMap.getId("test-cart")}/complete-cart` + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("Call CartService authorizePayment", () => { + expect(CartServiceMock.authorizePayment).toHaveBeenCalledTimes(1) + expect(CartServiceMock.authorizePayment).toHaveBeenCalledWith( + IdMap.getId("test-cart"), + { idempotency_key: "testkey" } + ) + }) + + it("Call OrderService createFromCart", () => { + expect(OrderServiceMock.createFromCart).toHaveBeenCalledTimes(1) + expect(OrderServiceMock.createFromCart).toHaveBeenCalledWith( + IdMap.getId("test-cart") + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the created order", () => { + expect(subject.body.data.id).toEqual(IdMap.getId("test-order")) + }) + }) + + describe("successfully completes a swap cart", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/store/carts/${IdMap.getId("swap-cart")}/complete-cart` + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("Call CartService authorizePayment", () => { + expect(CartServiceMock.authorizePayment).toHaveBeenCalledTimes(1) + expect(CartServiceMock.authorizePayment).toHaveBeenCalledWith( + IdMap.getId("swap-cart"), + { idempotency_key: "testkey" } + ) + }) + + it("Call SwapService registerCartCompletion", () => { + expect(SwapServiceMock.registerCartCompletion).toHaveBeenCalledTimes(1) + expect(SwapServiceMock.registerCartCompletion).toHaveBeenCalledWith( + "test-swap" + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the created order", () => { + expect(subject.body.data.id).toEqual("test-swap") + }) + }) + + describe("returns early if payment requires more work", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/store/carts/${IdMap.getId("test-cart2")}/complete-cart` + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("Call CartService authorizePayment", () => { + expect(CartServiceMock.authorizePayment).toHaveBeenCalledTimes(1) + expect(CartServiceMock.authorizePayment).toHaveBeenCalledWith( + IdMap.getId("test-cart2"), + { idempotency_key: "testkey" } + ) + }) + + it("Call CartService retrieve 0 times", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(0) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the created order", () => { + expect(subject.body.data.id).toEqual(IdMap.getId("test-cart2")) + expect(subject.body.data.payment_session.status).toEqual("requires_more") + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js index 9af2af862d..b8d3989e9c 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js @@ -22,19 +22,20 @@ describe("POST /store/carts", () => { it("calls CartService create", () => { expect(CartServiceMock.create).toHaveBeenCalledTimes(1) expect(CartServiceMock.create).toHaveBeenCalledWith({ - email: "", - customer_id: "", region_id: IdMap.getId("testRegion"), }) }) + it("calls CartService retrieve", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) + }) + it("returns 200", () => { expect(subject.status).toEqual(200) }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("regionCart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("regionCart")) }) }) @@ -89,22 +90,23 @@ describe("POST /store/carts", () => { }) it("calls line item generate", () => { - expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(2) - expect(LineItemServiceMock.generate).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - 3, - IdMap.getId("testRegion") - ) - expect(LineItemServiceMock.generate).toHaveBeenCalledWith( - IdMap.getId("testVariant1"), - 1, - IdMap.getId("testRegion") - ) + expect(LineItemServiceMock.create).toHaveBeenCalledTimes(2) + expect(LineItemServiceMock.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("testVariant"), + quantity: 3, + region_id: IdMap.getId("testRegion"), + cart_id: IdMap.getId("regionCart"), + }) + expect(LineItemServiceMock.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("testVariant1"), + quantity: 1, + region_id: IdMap.getId("testRegion"), + cart_id: IdMap.getId("regionCart"), + }) }) it("returns cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("regionCart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("regionCart")) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js b/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js index 103c99f180..f603758fb1 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js @@ -24,8 +24,8 @@ describe("POST /store/carts/:id", () => { jest.clearAllMocks() }) - it("calls CartService create", () => { - expect(CartServiceMock.addLineItem).toHaveBeenCalledTimes(1) + it("calls CartService retrieve", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(3) }) it("calls LineItemService generate", () => { @@ -43,8 +43,7 @@ describe("POST /store/carts/:id", () => { }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("emptyCart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("emptyCart")) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/create-payment-sessions.js b/packages/medusa/src/api/routes/store/carts/__tests__/create-payment-sessions.js index cecab8cd5f..bbf7c83669 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/create-payment-sessions.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/create-payment-sessions.js @@ -21,13 +21,16 @@ describe("POST /store/carts/:id/payment-sessions", () => { expect(CartServiceMock.setPaymentSessions).toHaveBeenCalledTimes(1) }) + it("calls Cart service retrieve", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) + }) + it("returns 200", () => { expect(subject.status).toEqual(200) }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("emptyCart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("emptyCart")) }) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js index a80a02100a..1bb9253c95 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js @@ -15,15 +15,11 @@ describe("GET /store/carts", () => { }) it("calls retrieve from CartService", () => { - expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(CartServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("emptyCart") - ) + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2) }) it("returns cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("emptyCart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("emptyCart")) }) it("returns 200 status", () => { @@ -44,7 +40,9 @@ describe("GET /store/carts", () => { it("calls get product from productSerice", () => { expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(CartServiceMock.retrieve).toHaveBeenCalledWith("none") + expect(CartServiceMock.retrieve).toHaveBeenCalledWith("none", { + relations: ["customer"], + }) }) it("returns 404 status", () => { diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/refresh-payment-session.js b/packages/medusa/src/api/routes/store/carts/__tests__/refresh-payment-session.js new file mode 100644 index 0000000000..ecf60ff92c --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/__tests__/refresh-payment-session.js @@ -0,0 +1,42 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { CartServiceMock } from "../../../../../services/__mocks__/cart" + +describe("POST /store/carts/:id/payment-session/update", () => { + describe("successfully updates the payment session", () => { + let subject + + beforeAll(async () => { + const cartId = IdMap.getId("cartWithPaySessions") + subject = await request( + "POST", + `/store/carts/${cartId}/payment-sessions/stripe/refresh`, + {} + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls CartService updatePaymentSession", () => { + expect(CartServiceMock.refreshPaymentSession).toHaveBeenCalledTimes(1) + expect(CartServiceMock.refreshPaymentSession).toHaveBeenCalledWith( + IdMap.getId("cartWithPaySessions"), + "stripe" + ) + }) + + it("calls CartService retrive", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the cart", () => { + expect(subject.body.cart.id).toEqual(IdMap.getId("cartWithPaySessions")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/update-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/update-cart.js index 34f22237da..b4020be2d7 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/update-cart.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/update-cart.js @@ -1,4 +1,5 @@ import { IdMap } from "medusa-test-utils" +import { defaultFields, defaultRelations } from ".." import { request } from "../../../../../helpers/test-request" import { CartServiceMock } from "../../../../../services/__mocks__/cart" @@ -41,44 +42,38 @@ describe("POST /store/carts/:id", () => { jest.clearAllMocks() }) - it("sets new region", () => { - expect(CartServiceMock.setRegion).toHaveBeenCalledTimes(1) - expect(CartServiceMock.setRegion).toHaveBeenCalledWith( + it("Call CartService update", () => { + expect(CartServiceMock.update).toHaveBeenCalledTimes(1) + expect(CartServiceMock.update).toHaveBeenCalledWith( IdMap.getId("emptyCart"), - IdMap.getId("testRegion"), - undefined + { + region_id: IdMap.getId("testRegion"), + email: "test@admin.com", + shipping_address: address, + billing_address: address, + discounts: [ + { + code: "TESTCODE", + }, + ], + } ) }) - it("updates email", () => { - expect(CartServiceMock.updateEmail).toHaveBeenCalledTimes(1) - expect(CartServiceMock.updateEmail).toHaveBeenCalledWith( + it("calls get product from productSerice", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2) + expect(CartServiceMock.retrieve).toHaveBeenCalledWith( IdMap.getId("emptyCart"), - "test@admin.com" + { + relations: ["payment_sessions"], + } ) - }) - - it("updates shipping address", () => { - expect(CartServiceMock.updateShippingAddress).toHaveBeenCalledTimes(1) - expect(CartServiceMock.updateShippingAddress).toHaveBeenCalledWith( + expect(CartServiceMock.retrieve).toHaveBeenCalledWith( IdMap.getId("emptyCart"), - address - ) - }) - - it("updates billing address", () => { - expect(CartServiceMock.updateBillingAddress).toHaveBeenCalledTimes(1) - expect(CartServiceMock.updateBillingAddress).toHaveBeenCalledWith( - IdMap.getId("emptyCart"), - address - ) - }) - - it("applies promo code", () => { - expect(CartServiceMock.applyDiscount).toHaveBeenCalledTimes(1) - expect(CartServiceMock.applyDiscount).toHaveBeenCalledWith( - IdMap.getId("emptyCart"), - "TESTCODE" + { + relations: defaultRelations, + select: defaultFields, + } ) }) @@ -87,32 +82,7 @@ describe("POST /store/carts/:id", () => { }) it("returns cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("emptyCart")) - expect(subject.body.cart.decorated).toEqual(true) - }) - }) - - describe("it bubbles errors", () => { - let subject - beforeAll(async () => { - subject = await request( - "POST", - `/store/carts/${IdMap.getId("emptyCart")}`, - { - payload: { - region_id: IdMap.getId("fail"), - }, - } - ) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("returns 404", () => { - expect(subject.status).toEqual(404) - expect(subject.body.message).toEqual("Region not found") + expect(subject.body.cart.id).toEqual(IdMap.getId("emptyCart")) }) }) @@ -127,11 +97,6 @@ describe("POST /store/carts/:id", () => { jest.clearAllMocks() }) - it("calls get product from productSerice", () => { - expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(CartServiceMock.retrieve).toHaveBeenCalledWith("none") - }) - it("returns 404", () => { expect(subject.status).toEqual(404) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js b/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js index 018c7054e1..c25f8e3f21 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js @@ -25,63 +25,22 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { jest.clearAllMocks() }) - it("calls CartService create", () => { + it("calls cartService.updateLineItem", () => { expect(CartServiceMock.updateLineItem).toHaveBeenCalledTimes(1) - }) - - it("calls LineItemService generate", () => { - expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) - expect(LineItemServiceMock.generate).toHaveBeenCalledWith( - IdMap.getId("eur-10-us-12"), - IdMap.getId("region-france"), - 3, - {} - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("fr-cart")) - expect(subject.body.cart.decorated).toEqual(true) - }) - }) - - describe("successfully updates a line item with metadata", () => { - let subject - - beforeAll(async () => { - const cartId = IdMap.getId("cartLineItemMetadata") - const lineId = IdMap.getId("lineWithMetadata") - subject = await request( - "POST", - `/store/carts/${cartId}/line-items/${lineId}`, + expect(CartServiceMock.updateLineItem).toHaveBeenCalledWith( + IdMap.getId("fr-cart"), + IdMap.getId("existingLine"), { - payload: { - quantity: 3, - }, + variant_id: IdMap.getId("eur-10-us-12"), + region_id: IdMap.getId("region-france"), + quantity: 3, + metadata: {}, } ) }) - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls CartService create", () => { - expect(CartServiceMock.updateLineItem).toHaveBeenCalledTimes(1) - }) - - it("calls LineItemService generate", () => { - expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) - expect(LineItemServiceMock.generate).toHaveBeenCalledWith( - IdMap.getId("eur-10-us-12"), - IdMap.getId("region-france"), - 3, - { status: "confirmed" } - ) + it("calls CartService retrieve", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(3) }) it("returns 200", () => { @@ -89,8 +48,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("cartLineItemMetadata")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("fr-cart")) }) }) @@ -128,8 +86,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("fr-cart")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("fr-cart")) }) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-method.js b/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-method.js index 7ec1ba34fe..a8be1310b0 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-method.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-method.js @@ -41,8 +41,7 @@ describe("POST /store/carts/:id/payment-method", () => { }) it("returns the cart", () => { - expect(subject.body.cart._id).toEqual(IdMap.getId("cartWithPaySessions")) - expect(subject.body.cart.decorated).toEqual(true) + expect(subject.body.cart.id).toEqual(IdMap.getId("cartWithPaySessions")) }) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-session.js b/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-session.js new file mode 100644 index 0000000000..487e788cf8 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/__tests__/update-payment-session.js @@ -0,0 +1,50 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { CartServiceMock } from "../../../../../services/__mocks__/cart" + +describe("POST /store/carts/:id/payment-session/update", () => { + describe("successfully updates the payment session", () => { + let subject + + beforeAll(async () => { + const cartId = IdMap.getId("cartWithPaySessions") + subject = await request( + "POST", + `/store/carts/${cartId}/payment-session/update`, + { + payload: { + session: { + data: "Something", + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls CartService updatePaymentSession", () => { + expect(CartServiceMock.updatePaymentSession).toHaveBeenCalledTimes(1) + expect(CartServiceMock.updatePaymentSession).toHaveBeenCalledWith( + IdMap.getId("cartWithPaySessions"), + { + data: "Something", + } + ) + }) + + it("calls CartService retrive", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the cart", () => { + expect(subject.body.cart.id).toEqual(IdMap.getId("cartWithPaySessions")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/carts/add-shipping-method.js b/packages/medusa/src/api/routes/store/carts/add-shipping-method.js index db5702a01b..6c71cde1cf 100644 --- a/packages/medusa/src/api/routes/store/carts/add-shipping-method.js +++ b/packages/medusa/src/api/routes/store/carts/add-shipping-method.js @@ -1,5 +1,6 @@ import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params @@ -17,12 +18,26 @@ export default async (req, res) => { } try { + const manager = req.scope.resolve("manager") const cartService = req.scope.resolve("cartService") - await cartService.addShippingMethod(id, value.option_id, value.data) + await manager.transaction(async m => { + await cartService + .withTransaction(m) + .addShippingMethod(id, value.option_id, value.data) + const updated = await cartService.withTransaction(m).retrieve(id, { + relations: ["payment_sessions"], + }) - let cart = await cartService.retrieve(id) - cart = await cartService.decorate(cart, [], ["region"]) + if (updated.payment_sessions?.length) { + await cartService.withTransaction(m).setPaymentSessions(id) + } + }) + + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/carts/complete-cart.js b/packages/medusa/src/api/routes/store/carts/complete-cart.js new file mode 100644 index 0000000000..41341f4d63 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/complete-cart.js @@ -0,0 +1,203 @@ +import { MedusaError } from "medusa-core-utils" + +export default async (req, res) => { + const { id } = req.params + + const idempotencyKeyService = req.scope.resolve("idempotencyKeyService") + + const headerKey = req.get("Idempotency-Key") || "" + + let idempotencyKey + try { + idempotencyKey = await idempotencyKeyService.initializeRequest( + headerKey, + req.method, + req.params, + req.path + ) + } catch (error) { + console.log(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 { + const cartService = req.scope.resolve("cartService") + const orderService = req.scope.resolve("orderService") + const swapService = req.scope.resolve("swapService") + + let inProgress = true + let err = false + + while (inProgress) { + switch (idempotencyKey.recovery_point) { + case "started": { + const { key, error } = await idempotencyKeyService.workStage( + idempotencyKey.idempotency_key, + async manager => { + const cart = await cartService + .withTransaction(manager) + .authorizePayment(id, { + ...req.request_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 }, + } + } + } + + 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 => { + const cart = await cartService + .withTransaction(manager) + .retrieve(id, { + select: ["total"], + relations: ["payment", "payment_sessions"], + }) + + let order + + // If cart is part of swap, we register swap as complete + switch (cart.type) { + case "swap": { + const swapId = cart.metadata?.swap_id + order = await swapService + .withTransaction(manager) + .registerCartCompletion(swapId) + + order = await swapService + .withTransaction(manager) + .retrieve(order.id, { relations: ["shipping_address"] }) + + return { + response_code: 200, + response_body: { data: order }, + } + } + // case "payment_link": + default: { + if (!cart.payment && cart.total > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cart payment not authorized` + ) + } + + 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 }, + } + } 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 }, + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + break + } + + case "finished": { + inProgress = false + break + } + + default: + idempotencyKey = await idempotencyKeyService.update( + idempotencyKey.idempotency_key, + { + recovery_point: "finished", + response_code: 500, + response_body: { message: "Unknown recovery point" }, + } + ) + break + } + } + + if (err) { + throw err + } + + res.status(idempotencyKey.response_code).json(idempotencyKey.response_body) + } catch (error) { + console.log(error) + throw error + } +} diff --git a/packages/medusa/src/api/routes/store/carts/create-cart.js b/packages/medusa/src/api/routes/store/carts/create-cart.js index de1db2682c..e22f7dc7ec 100644 --- a/packages/medusa/src/api/routes/store/carts/create-cart.js +++ b/packages/medusa/src/api/routes/store/carts/create-cart.js @@ -1,8 +1,9 @@ import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const schema = Validator.object().keys({ - region_id: Validator.string(), + region_id: Validator.string().optional(), country_code: Validator.string().optional(), items: Validator.array() .items({ @@ -21,52 +22,57 @@ export default async (req, res) => { const lineItemService = req.scope.resolve("lineItemService") const cartService = req.scope.resolve("cartService") - // Add a default region if no region has been specified - let regionId = value.region_id - if (!value.region_id) { - const regionService = req.scope.resolve("regionService") - const regions = await regionService.list() - regionId = regions[0]._id - } + const entityManager = req.scope.resolve("manager") - let customerId = "" - let email = "" - if (req.user && req.user.customer_id) { - const customerService = req.scope.resolve("customerService") - const customer = await customerService.retrieve(req.user.customer_id) - customerId = customer._id - email = customer.email - } - - const toCreate = { - region_id: regionId, - customer_id: customerId, - email, - } - - if (value.country_code) { - toCreate.shipping_address = { - country_code: value.country_code.toUpperCase(), + await entityManager.transaction(async manager => { + // Add a default region if no region has been specified + let regionId = value.region_id + if (!value.region_id) { + const regionService = req.scope.resolve("regionService") + const regions = await regionService.withTransaction(manager).list({}) + regionId = regions[0].id } - } - let cart = await cartService.create(toCreate) - if (value.items) { - await Promise.all( - value.items.map(async i => { - const lineItem = await lineItemService.generate( - i.variant_id, - i.quantity, - value.region_id - ) - await cartService.addLineItem(cart._id, lineItem) - }) - ) - } + const toCreate = { + region_id: regionId, + } - cart = await cartService.retrieve(cart._id) - cart = await cartService.decorate(cart, [], ["region"]) - res.status(200).json({ cart }) + if (req.user && req.user.customer_id) { + const customerService = req.scope.resolve("customerService") + const customer = await customerService + .withTransaction(manager) + .retrieve(req.user.customer_id) + toCreate.customer_id = customer.id + toCreate.email = customer.email + } + + if (value.country_code) { + toCreate.shipping_address = { + country_code: value.country_code.toLowerCase(), + } + } + + let cart = await cartService.withTransaction(manager).create(toCreate) + if (value.items) { + await Promise.all( + value.items.map(async i => { + await lineItemService.withTransaction(manager).create({ + cart_id: cart.id, + variant_id: i.variant_id, + quantity: i.quantity, + region_id: value.region_id, + }) + }) + ) + } + + cart = await cartService.withTransaction(manager).retrieve(cart.id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ cart }) + }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/store/carts/create-line-item.js b/packages/medusa/src/api/routes/store/carts/create-line-item.js index a52604b811..711ff5c506 100644 --- a/packages/medusa/src/api/routes/store/carts/create-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/create-line-item.js @@ -1,4 +1,5 @@ import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params @@ -15,20 +16,34 @@ export default async (req, res) => { } try { + const manager = req.scope.resolve("manager") const lineItemService = req.scope.resolve("lineItemService") const cartService = req.scope.resolve("cartService") + let cart = await cartService.retrieve(id) - const lineItem = await lineItemService.generate( - value.variant_id, - cart.region_id, - value.quantity, - value.metadata - ) - await cartService.addLineItem(cart._id, lineItem) + await manager.transaction(async m => { + const line = await lineItemService.generate( + value.variant_id, + cart.region_id, + value.quantity, + value.metadata + ) + await cartService.withTransaction(m).addLineItem(id, line) - cart = await cartService.retrieve(cart._id) - cart = await cartService.decorate(cart, [], ["region"]) + const updated = await cartService.withTransaction(m).retrieve(id, { + relations: ["payment_sessions"], + }) + + if (updated.payment_sessions?.length) { + await cartService.withTransaction(m).setPaymentSessions(id) + } + }) + + cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js b/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js index bc95e5c206..7f9f8aa16d 100644 --- a/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js +++ b/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js @@ -1,19 +1,20 @@ +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { const { id } = req.params try { const cartService = req.scope.resolve("cartService") - // Ask the cart service to set payment sessions await cartService.setPaymentSessions(id) - // return the updated cart - let cart = await cartService.retrieve(id) - cart = await cartService.decorate(cart, [], ["region"]) + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { - console.log(err) throw err } } diff --git a/packages/medusa/src/api/routes/store/carts/delete-discount.js b/packages/medusa/src/api/routes/store/carts/delete-discount.js index b404af55b4..8b0b3573dc 100644 --- a/packages/medusa/src/api/routes/store/carts/delete-discount.js +++ b/packages/medusa/src/api/routes/store/carts/delete-discount.js @@ -1,13 +1,29 @@ -import { Validator, MedusaError } from "medusa-core-utils" - +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, code } = req.params try { + const manager = req.scope.resolve("manager") const cartService = req.scope.resolve("cartService") - let cart = await cartService.removeDiscount(id, code) - cart = await cartService.decorate(cart, [], ["region"]) + await manager.transaction(async m => { + // Remove the discount + await cartService.withTransaction(m).removeDiscount(id, code) + + // If the cart has payment sessions update these + const updated = await cartService.withTransaction(m).retrieve(id, { + relations: ["payment_sessions"], + }) + + if (updated.payment_sessions?.length) { + await cartService.withTransaction(m).setPaymentSessions(id) + } + }) + + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/carts/delete-line-item.js b/packages/medusa/src/api/routes/store/carts/delete-line-item.js index 80cf633895..2d95414fd7 100644 --- a/packages/medusa/src/api/routes/store/carts/delete-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/delete-line-item.js @@ -1,13 +1,29 @@ -import { Validator, MedusaError } from "medusa-core-utils" - +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, line_id } = req.params try { + const manager = req.scope.resolve("manager") const cartService = req.scope.resolve("cartService") - let cart = await cartService.removeLineItem(id, line_id) - cart = await cartService.decorate(cart, [], ["region"]) + await manager.transaction(async m => { + // Remove the line item + await cartService.withTransaction(m).removeLineItem(id, line_id) + + // If the cart has payment sessions update these + const updated = await cartService.withTransaction(m).retrieve(id, { + relations: ["payment_sessions"], + }) + + if (updated.payment_sessions?.length) { + await cartService.withTransaction(m).setPaymentSessions(id) + } + }) + + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/carts/delete-payment-session.js b/packages/medusa/src/api/routes/store/carts/delete-payment-session.js index 55104d4bd5..058c19b3ad 100644 --- a/packages/medusa/src/api/routes/store/carts/delete-payment-session.js +++ b/packages/medusa/src/api/routes/store/carts/delete-payment-session.js @@ -1,11 +1,16 @@ +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { const { id, provider_id } = req.params try { const cartService = req.scope.resolve("cartService") - let cart = await cartService.deletePaymentSession(id, provider_id) - cart = await cartService.decorate(cart, [], ["region"]) + await cartService.deletePaymentSession(id, provider_id) + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/carts/get-cart.js b/packages/medusa/src/api/routes/store/carts/get-cart.js index e91092db20..986abc6198 100644 --- a/packages/medusa/src/api/routes/store/carts/get-cart.js +++ b/packages/medusa/src/api/routes/store/carts/get-cart.js @@ -1,8 +1,12 @@ +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params try { const cartService = req.scope.resolve("cartService") - let cart = await cartService.retrieve(id) + + let cart = await cartService.retrieve(id, { + relations: ["customer"], + }) // If there is a logged in user add the user to the cart if (req.user && req.user.customer_id) { @@ -11,15 +15,17 @@ export default async (req, res) => { !cart.email || cart.customer_id !== req.user.customer_id ) { - const customerService = req.scope.resolve("customerService") - const customer = await customerService.retrieve(req.user.customer_id) - - cart = await cartService.updateCustomerId(id, customer._id) - cart = await cartService.updateEmail(id, customer.email) + await cartService.update(id, { + customer_id: req.user.customer_id, + }) } } - cart = await cartService.decorate(cart, [], ["region"]) + cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + res.json({ cart }) } catch (err) { throw err diff --git a/packages/medusa/src/api/routes/store/carts/index.js b/packages/medusa/src/api/routes/store/carts/index.js index 80d6c123f6..0ec18e79fe 100644 --- a/packages/medusa/src/api/routes/store/carts/index.js +++ b/packages/medusa/src/api/routes/store/carts/index.js @@ -24,6 +24,11 @@ export default (app, container) => { route.post("/:id", middlewares.wrap(require("./update-cart").default)) + route.post( + "/:id/complete-cart", + middlewares.wrap(require("./complete-cart").default) + ) + // Line items route.post( "/:id/line-items", @@ -49,10 +54,26 @@ export default (app, container) => { middlewares.wrap(require("./create-payment-sessions").default) ) + route.post( + "/:id/payment-session/update", + middlewares.wrap(require("./update-payment-session").default) + ) + route.delete( "/:id/payment-sessions/:provider_id", middlewares.wrap(require("./delete-payment-session").default) ) + + route.post( + "/:id/payment-sessions/:provider_id/refresh", + middlewares.wrap(require("./refresh-payment-session").default) + ) + + route.post( + "/:id/payment-session", + middlewares.wrap(require("./set-payment-session").default) + ) + route.post( "/:id/payment-method", middlewares.wrap(require("./update-payment-method").default) @@ -66,3 +87,27 @@ export default (app, container) => { return app } + +export const defaultFields = [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "gift_card_total", + "total", +] + +export const defaultRelations = [ + "gift_cards", + "region", + "items", + "payment", + "shipping_address", + "billing_address", + "region.countries", + "region.payment_providers", + "shipping_methods", + "payment_sessions", + "shipping_methods.shipping_option", + "discounts", +] diff --git a/packages/medusa/src/api/routes/store/carts/refresh-payment-session.js b/packages/medusa/src/api/routes/store/carts/refresh-payment-session.js new file mode 100644 index 0000000000..7e21b915c6 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/refresh-payment-session.js @@ -0,0 +1,30 @@ +export default async (req, res) => { + const { id, provider_id } = req.params + + try { + const cartService = req.scope.resolve("cartService") + + await cartService.refreshPaymentSession(id, provider_id) + const cart = await cartService.retrieve(id, { + select: [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "total", + ], + relations: [ + "region", + "region.countries", + "region.payment_providers", + "shipping_methods", + "payment_sessions", + "shipping_methods.shipping_option", + ], + }) + + res.status(200).json({ cart }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/carts/set-payment-session.js b/packages/medusa/src/api/routes/store/carts/set-payment-session.js new file mode 100644 index 0000000000..cbcf4937b3 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/set-payment-session.js @@ -0,0 +1,29 @@ +import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + provider_id: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const cartService = req.scope.resolve("cartService") + + let cart = await cartService.setPaymentSession(id, value.provider_id) + cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ cart }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/carts/update-cart.js b/packages/medusa/src/api/routes/store/carts/update-cart.js index b8b14a24cf..70c568631c 100644 --- a/packages/medusa/src/api/routes/store/carts/update-cart.js +++ b/packages/medusa/src/api/routes/store/carts/update-cart.js @@ -1,18 +1,30 @@ import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" + export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ - region_id: Validator.string(), + region_id: Validator.string().optional(), country_code: Validator.string().optional(), - email: Validator.string().email(), - billing_address: Validator.address(), - shipping_address: Validator.address(), - discounts: Validator.array().items({ - code: Validator.string(), - }), + email: Validator.string() + .email() + .optional(), + billing_address: Validator.object().optional(), + shipping_address: Validator.object().optional(), + gift_cards: Validator.array() + .items({ + code: Validator.string(), + }) + .optional(), + discounts: Validator.array() + .items({ + code: Validator.string(), + }) + .optional(), + customer_id: Validator.string().optional(), }) const { value, error } = schema.validate(req.body) @@ -20,42 +32,32 @@ export default async (req, res) => { throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) } - const cartService = req.scope.resolve("cartService") - const oldCart = await cartService.retrieve(id) - if (!oldCart) { - res.sendStatus(404) - return - } - try { - if (value.region_id) { - await cartService.setRegion(id, value.region_id, value.country_code) - } + const manager = req.scope.resolve("manager") + const cartService = req.scope.resolve("cartService") - if (value.email) { - await cartService.updateEmail(id, value.email) - } + await manager.transaction(async m => { + // Update the cart + await cartService.withTransaction(m).update(id, value) - if (!_.isEmpty(value.shipping_address)) { - await cartService.updateShippingAddress(id, value.shipping_address) - } + // If the cart has payment sessions update these + const updated = await cartService.withTransaction(m).retrieve(id, { + relations: ["payment_sessions"], + }) - if (!_.isEmpty(value.billing_address)) { - await cartService.updateBillingAddress(id, value.billing_address) - } + if (updated.payment_sessions?.length && !value.region_id) { + await cartService.withTransaction(m).setPaymentSessions(id) + } + }) - if (value.discounts && value.discounts.length) { - await Promise.all( - value.discounts.map(async ({ code }) => - cartService.applyDiscount(id, code) - ) - ) - } + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) - let newCart = await cartService.retrieve(id) - const data = await cartService.decorate(newCart, [], ["region"]) - res.json({ cart: data }) + res.json({ cart }) } catch (err) { + console.log(err) throw err } } diff --git a/packages/medusa/src/api/routes/store/carts/update-line-item.js b/packages/medusa/src/api/routes/store/carts/update-line-item.js index 9c47d4603a..0de2ad4c04 100644 --- a/packages/medusa/src/api/routes/store/carts/update-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/update-line-item.js @@ -1,4 +1,5 @@ import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id, line_id } = req.params @@ -9,40 +10,54 @@ export default async (req, res) => { const { value, error } = schema.validate(req.body) if (error) { - console.log(error) throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) } try { - const lineItemService = req.scope.resolve("lineItemService") + const manager = req.scope.resolve("manager") const cartService = req.scope.resolve("cartService") - let cart - if (value.quantity === 0) { - cart = await cartService.removeLineItem(id, line_id) - } else { - cart = await cartService.retrieve(id) + await manager.transaction(async m => { + // If the quantity is 0 that is effectively deletion + if (value.quantity === 0) { + await cartService.withTransaction(m).removeLineItem(id, line_id) + } else { + const cart = await cartService.retrieve(id, { relations: ["items"] }) - const existing = cart.items.find(i => i._id.equals(line_id)) - if (!existing) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Could not find the line item" - ) + const existing = cart.items.find(i => i.id === line_id) + if (!existing) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Could not find the line item" + ) + } + + const lineItemUpdate = { + variant_id: existing.variant.id, + region_id: cart.region_id, + quantity: value.quantity, + metadata: existing.metadata || {}, + } + + await cartService + .withTransaction(m) + .updateLineItem(id, line_id, lineItemUpdate) } - const lineItem = await lineItemService.generate( - existing.content.variant._id, - cart.region_id, - value.quantity, - existing.metadata || {} - ) + // If the cart has payment sessions update these + const updated = await cartService.withTransaction(m).retrieve(id, { + relations: ["payment_sessions"], + }) - cart = await cartService.updateLineItem(cart._id, line_id, lineItem) - } - - cart = await cartService.decorate(cart, [], ["region"]) + if (updated.payment_sessions?.length) { + await cartService.withTransaction(m).setPaymentSessions(id) + } + }) + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { throw err diff --git a/packages/medusa/src/api/routes/store/carts/update-payment-method.js b/packages/medusa/src/api/routes/store/carts/update-payment-method.js index 258fa7ee52..e191b433ea 100644 --- a/packages/medusa/src/api/routes/store/carts/update-payment-method.js +++ b/packages/medusa/src/api/routes/store/carts/update-payment-method.js @@ -1,4 +1,5 @@ import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" export default async (req, res) => { const { id } = req.params @@ -17,7 +18,10 @@ export default async (req, res) => { const cartService = req.scope.resolve("cartService") let cart = await cartService.setPaymentMethod(id, value) - cart = await cartService.decorate(cart, [], ["region"]) + cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.status(200).json({ cart }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/carts/update-payment-session.js b/packages/medusa/src/api/routes/store/carts/update-payment-session.js new file mode 100644 index 0000000000..3a503bd16c --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/update-payment-session.js @@ -0,0 +1,30 @@ +import { Validator, MedusaError } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" + +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + session: Validator.object().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const cartService = req.scope.resolve("cartService") + + await cartService.updatePaymentSession(id, value.session) + + const cart = await cartService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.status(200).json({ cart }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/customers/__tests__/create-customer.js b/packages/medusa/src/api/routes/store/customers/__tests__/create-customer.js index d335b94bb6..35ae67344d 100644 --- a/packages/medusa/src/api/routes/store/customers/__tests__/create-customer.js +++ b/packages/medusa/src/api/routes/store/customers/__tests__/create-customer.js @@ -1,3 +1,4 @@ +import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { CustomerServiceMock } from "../../../../../services/__mocks__/customer" @@ -29,9 +30,16 @@ describe("POST /store/customers", () => { }) }) - it("returns customer decorated", () => { + it("calls CustomerService retrieve", () => { + expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("lebron"), + ["orders", "shipping_addresses"] + ) + }) + + it("returns customer", () => { expect(subject.body.customer.email).toEqual("lebron@james.com") - expect(subject.body.customer.decorated).toEqual(true) }) }) diff --git a/packages/medusa/src/api/routes/store/customers/__tests__/reset-password.js b/packages/medusa/src/api/routes/store/customers/__tests__/reset-password.js index a9f4220a8f..8b8037e6cc 100644 --- a/packages/medusa/src/api/routes/store/customers/__tests__/reset-password.js +++ b/packages/medusa/src/api/routes/store/customers/__tests__/reset-password.js @@ -4,7 +4,7 @@ import { request } from "../../../../../helpers/test-request" import { CustomerServiceMock } from "../../../../../services/__mocks__/customer" describe("POST /store/customers/password-reset", () => { - describe("successfully creates a customer", () => { + describe("successfully udates customer password", () => { let subject beforeAll(async () => { subject = await request("POST", `/store/customers/password-reset`, { @@ -20,7 +20,7 @@ describe("POST /store/customers/password-reset", () => { jest.clearAllMocks() }) - it("calls CustomerService create", () => { + it("calls CustomerService update", () => { expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.update).toHaveBeenCalledWith( IdMap.getId("lebron"), @@ -30,9 +30,15 @@ describe("POST /store/customers/password-reset", () => { ) }) - it("returns customer decorated", () => { + it("calls CustomerService retrieve", () => { + expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("lebron") + ) + }) + + it("returns customer ", () => { expect(subject.body.customer.email).toEqual("lebron@james.com") - expect(subject.body.customer.decorated).toEqual(true) }) }) diff --git a/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js b/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js index 0894707b09..af2bc31ba6 100644 --- a/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js +++ b/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js @@ -27,7 +27,7 @@ describe("POST /store/customers/:id", () => { jest.clearAllMocks() }) - it("calls CustomerService create", () => { + it("calls CustomerService update", () => { expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.update).toHaveBeenCalledWith( IdMap.getId("lebron"), @@ -38,9 +38,16 @@ describe("POST /store/customers/:id", () => { ) }) - it("returns product decorated", () => { - expect(subject.body.customer.first_name).toEqual("LeBron") - expect(subject.body.customer.decorated).toEqual(true) + it("calls CustomerService retrieve", () => { + expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("lebron"), + ["orders", "shipping_addresses"] + ) + }) + + it("returns customer", () => { + expect(subject.body.customer.id).toEqual(IdMap.getId("lebron")) }) it("status code 200", () => { diff --git a/packages/medusa/src/api/routes/store/customers/create-address.js b/packages/medusa/src/api/routes/store/customers/create-address.js index b1d507be8e..a058b8a80d 100644 --- a/packages/medusa/src/api/routes/store/customers/create-address.js +++ b/packages/medusa/src/api/routes/store/customers/create-address.js @@ -1,10 +1,10 @@ import { Validator, MedusaError } from "medusa-core-utils" export default async (req, res) => { - const { id, address_id } = req.params + const { id } = req.params const schema = Validator.object().keys({ - address: Validator.address(), + address: Validator.address().required(), }) const { value, error } = schema.validate(req.body) @@ -12,15 +12,15 @@ export default async (req, res) => { throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) } - const customerService = req.scope.resolve("customerService") try { - const customer = await customerService.addAddress(id, value.address) - const data = await customerService.decorate( - customer, - ["email", "first_name", "last_name", "shipping_addresses"], - ["orders"] - ) - res.json({ customer: data }) + const customerService = req.scope.resolve("customerService") + + let customer = await customerService.addAddress(id, value.address) + customer = await customerService.retrieve(id, { + relations: ["orders", "shipping_addresses"], + }) + + res.status(200).json({ customer }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/store/customers/create-customer.js b/packages/medusa/src/api/routes/store/customers/create-customer.js index c96c25d528..7a44520029 100644 --- a/packages/medusa/src/api/routes/store/customers/create-customer.js +++ b/packages/medusa/src/api/routes/store/customers/create-customer.js @@ -19,7 +19,7 @@ export default async (req, res) => { } try { const customerService = req.scope.resolve("customerService") - const customer = await customerService.create(value) + let customer = await customerService.create(value) // Add JWT to cookie req.session.jwt = jwt.sign( @@ -30,16 +30,12 @@ export default async (req, res) => { } ) - const data = await customerService.decorate(customer, [ - "_id", - "email", + customer = await customerService.retrieve(customer.id, [ "orders", "shipping_addresses", - "first_name", - "last_name", - "phone", ]) - res.status(201).json({ customer: data }) + + res.status(200).json({ customer }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/store/customers/delete-address.js b/packages/medusa/src/api/routes/store/customers/delete-address.js index c38b5bee00..eb53c1aabe 100644 --- a/packages/medusa/src/api/routes/store/customers/delete-address.js +++ b/packages/medusa/src/api/routes/store/customers/delete-address.js @@ -3,12 +3,12 @@ export default async (req, res) => { const customerService = req.scope.resolve("customerService") try { - const customer = await customerService.removeAddress(id, address_id) - const data = await customerService.decorate( - customer, - ["email", "first_name", "last_name", "shipping_addresses"], - ["orders"] - ) + let customer = await customerService.removeAddress(id, address_id) + + customer = await customerService.retrieve(id, { + relations: ["orders", "shipping_addresses"], + }) + res.json({ customer: data }) } catch (err) { throw err diff --git a/packages/medusa/src/api/routes/store/customers/get-customer.js b/packages/medusa/src/api/routes/store/customers/get-customer.js index 3ad715c2bf..e2677231d5 100644 --- a/packages/medusa/src/api/routes/store/customers/get-customer.js +++ b/packages/medusa/src/api/routes/store/customers/get-customer.js @@ -2,12 +2,9 @@ export default async (req, res) => { const { id } = req.params try { const customerService = req.scope.resolve("customerService") - let customer = await customerService.retrieve(id) - customer = customerService.decorate( - customer, - ["email", "first_name", "last_name", "shipping_addresses", "phone"], - ["orders"] - ) + const customer = await customerService.retrieve(id, { + relations: ["orders", "shipping_addresses"], + }) res.json({ customer }) } catch (err) { throw err diff --git a/packages/medusa/src/api/routes/store/customers/get-payment-methods.js b/packages/medusa/src/api/routes/store/customers/get-payment-methods.js index 08936c0ed5..0f5a9a662a 100644 --- a/packages/medusa/src/api/routes/store/customers/get-payment-methods.js +++ b/packages/medusa/src/api/routes/store/customers/get-payment-methods.js @@ -7,7 +7,7 @@ export default async (req, res) => { let customer = await customerService.retrieve(id) - const store = await storeService.retrieve() + const store = await storeService.retrieve(["payment_providers"]) const methods = await Promise.all( store.payment_providers.map(async next => { diff --git a/packages/medusa/src/api/routes/store/customers/reset-password-token.js b/packages/medusa/src/api/routes/store/customers/reset-password-token.js index 37c7555f6c..75c36a2300 100644 --- a/packages/medusa/src/api/routes/store/customers/reset-password-token.js +++ b/packages/medusa/src/api/routes/store/customers/reset-password-token.js @@ -17,7 +17,7 @@ export default async (req, res) => { const customer = await customerService.retrieveByEmail(value.email) // Will generate a token and send it to the customer via an email privder - await customerService.generateResetPasswordToken(customer._id) + await customerService.generateResetPasswordToken(customer.id) res.sendStatus(204) } catch (error) { diff --git a/packages/medusa/src/api/routes/store/customers/reset-password.js b/packages/medusa/src/api/routes/store/customers/reset-password.js index 9ca9ca51d5..47a13eab74 100644 --- a/packages/medusa/src/api/routes/store/customers/reset-password.js +++ b/packages/medusa/src/api/routes/store/customers/reset-password.js @@ -17,19 +17,20 @@ export default async (req, res) => { try { const customerService = req.scope.resolve("customerService") - const customer = await customerService.retrieveByEmail(value.email) + let customer = await customerService.retrieveByEmail(value.email) const decodedToken = await jwt.verify(value.token, customer.password_hash) - if (!decodedToken || !customer._id.equals(decodedToken.customer_id)) { + if (!decodedToken || customer.id !== decodedToken.customer_id) { res.status(401).send("Invalid or expired password reset token") return } - const updated = await customerService.update(customer._id, { + await customerService.update(customer.id, { password: value.password, }) - const data = await customerService.decorate(customer) - res.status(200).json({ customer: data }) + + customer = await customerService.retrieve(customer.id) + res.status(200).json({ customer }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/store/customers/update-address.js b/packages/medusa/src/api/routes/store/customers/update-address.js index ecc6d74c19..334123f40d 100644 --- a/packages/medusa/src/api/routes/store/customers/update-address.js +++ b/packages/medusa/src/api/routes/store/customers/update-address.js @@ -4,7 +4,7 @@ export default async (req, res) => { const { id, address_id } = req.params const schema = Validator.object().keys({ - address: Validator.address(), + address: Validator.address().required(), }) const { value, error } = schema.validate(req.body) @@ -14,17 +14,17 @@ export default async (req, res) => { const customerService = req.scope.resolve("customerService") try { - const customer = await customerService.updateAddress( + let customer = await customerService.updateAddress( id, address_id, value.address ) - const data = await customerService.decorate( - customer, - ["email", "first_name", "last_name", "shipping_addresses"], - ["orders"] - ) - res.json({ customer: data }) + + customer = await customerService.retrieve(id, { + relations: ["orders", "shipping_addresses"], + }) + + res.json({ customer }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/store/customers/update-customer.js b/packages/medusa/src/api/routes/store/customers/update-customer.js index f463a31ecc..e9cc8a1755 100644 --- a/packages/medusa/src/api/routes/store/customers/update-customer.js +++ b/packages/medusa/src/api/routes/store/customers/update-customer.js @@ -17,13 +17,14 @@ export default async (req, res) => { try { const customerService = req.scope.resolve("customerService") - const customer = await customerService.update(id, value) - const data = await customerService.decorate( - customer, - ["email", "first_name", "last_name", "shipping_addresses", "phone"], - ["orders"] - ) - res.status(200).json({ customer: data }) + let customer = await customerService.update(id, value) + + customer = await customerService.retrieve(customer.id, [ + "orders", + "shipping_addresses", + ]) + + res.status(200).json({ customer }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/store/orders/__tests__/create-order.js b/packages/medusa/src/api/routes/store/orders/__tests__/create-order.js deleted file mode 100644 index 1cc93d7a5d..0000000000 --- a/packages/medusa/src/api/routes/store/orders/__tests__/create-order.js +++ /dev/null @@ -1,27 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { OrderServiceMock } from "../../../../../services/__mocks__/order" -import { carts } from "../../../../../services/__mocks__/cart" - -describe("POST /store/orders", () => { - describe("successful creation", () => { - let subject - - beforeAll(async () => { - subject = await request("POST", "/store/orders", { - payload: { - cartId: IdMap.getId("fr-cart"), - }, - }) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service create", () => { - expect(OrderServiceMock.createFromCart).toHaveBeenCalledTimes(1) - expect(OrderServiceMock.createFromCart).toHaveBeenCalledWith(carts.frCart) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/store/orders/__tests__/get-order-by-cart.js b/packages/medusa/src/api/routes/store/orders/__tests__/get-order-by-cart.js new file mode 100644 index 0000000000..9d0be5c0cf --- /dev/null +++ b/packages/medusa/src/api/routes/store/orders/__tests__/get-order-by-cart.js @@ -0,0 +1,35 @@ +import { IdMap } from "medusa-test-utils" +import { defaultFields, defaultRelations } from ".." +import { request } from "../../../../../helpers/test-request" +import { OrderServiceMock } from "../../../../../services/__mocks__/order" + +describe("GET /store/orders", () => { + describe("successfully gets an order", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/store/orders/cart/${IdMap.getId("test-cart")}` + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService retrieve", () => { + expect(OrderServiceMock.retrieveByCartId).toHaveBeenCalledTimes(1) + expect( + OrderServiceMock.retrieveByCartId + ).toHaveBeenCalledWith(IdMap.getId("test-cart"), { + select: defaultFields, + relations: defaultRelations, + }) + }) + + it("returns order", () => { + expect(subject.body.order.id).toEqual(IdMap.getId("test-order")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/orders/__tests__/get-order.js b/packages/medusa/src/api/routes/store/orders/__tests__/get-order.js index 61cbe092e1..3727078464 100644 --- a/packages/medusa/src/api/routes/store/orders/__tests__/get-order.js +++ b/packages/medusa/src/api/routes/store/orders/__tests__/get-order.js @@ -1,4 +1,5 @@ import { IdMap } from "medusa-test-utils" +import { defaultFields, defaultRelations } from ".." import { request } from "../../../../../helpers/test-request" import { OrderServiceMock } from "../../../../../services/__mocks__/order" @@ -20,12 +21,13 @@ describe("GET /store/orders", () => { it("calls orderService retrieve", () => { expect(OrderServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(OrderServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("test-order") + IdMap.getId("test-order"), + { select: defaultFields, relations: defaultRelations } ) }) it("returns order", () => { - expect(subject.body.order._id).toEqual(IdMap.getId("test-order")) + expect(subject.body.order.id).toEqual(IdMap.getId("test-order")) }) }) }) diff --git a/packages/medusa/src/api/routes/store/orders/create-order.js b/packages/medusa/src/api/routes/store/orders/create-order.js deleted file mode 100644 index 2ba2e9bdc1..0000000000 --- a/packages/medusa/src/api/routes/store/orders/create-order.js +++ /dev/null @@ -1,59 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const schema = Validator.object().keys({ - cartId: Validator.string().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const cartService = req.scope.resolve("cartService") - const orderService = req.scope.resolve("orderService") - - const cart = await cartService.retrieve(value.cartId) - let order = await orderService.createFromCart(cart) - order = await orderService.decorate(order, [ - "status", - "fulfillment_status", - "payment_status", - "email", - "billing_address", - "shipping_address", - "items", - "region", - "discounts", - "customer_id", - "payment_method", - "shipping_methods", - "metadata", - ]) - - res.status(200).json({ order }) - } catch (err) { - // If something fails it might be because the order has already been created - // if it has we find it from the cart id - const orderService = req.scope.resolve("orderService") - let order = await orderService.retrieveByCartId(value.cartId) - order = await orderService.decorate(order, [ - "status", - "fulfillment_status", - "payment_status", - "email", - "billing_address", - "shipping_address", - "items", - "region", - "discounts", - "customer_id", - "payment_method", - "shipping_methods", - "metadata", - ]) - - res.status(200).json({ order }) - } -} diff --git a/packages/medusa/src/api/routes/store/orders/get-order-by-cart.js b/packages/medusa/src/api/routes/store/orders/get-order-by-cart.js new file mode 100644 index 0000000000..de79de24f8 --- /dev/null +++ b/packages/medusa/src/api/routes/store/orders/get-order-by-cart.js @@ -0,0 +1,17 @@ +import { defaultFields, defaultRelations } from "." + +export default async (req, res) => { + const { cart_id } = req.params + + try { + const orderService = req.scope.resolve("orderService") + const order = await orderService.retrieveByCartId(cart_id, { + select: defaultFields, + relations: defaultRelations, + }) + + res.json({ order }) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/store/orders/get-order.js b/packages/medusa/src/api/routes/store/orders/get-order.js index 3585f6e85d..f2d8dcc089 100644 --- a/packages/medusa/src/api/routes/store/orders/get-order.js +++ b/packages/medusa/src/api/routes/store/orders/get-order.js @@ -1,36 +1,18 @@ +import { defaultRelations, defaultFields } from "./index" + export default async (req, res) => { const { id } = req.params try { const orderService = req.scope.resolve("orderService") - let order = await orderService.retrieve(id) - order = await orderService.decorate( - order, - [ - "status", - "fulfillment_status", - "payment_status", - "email", - "billing_address", - "shipping_address", - "items", - "region", - "discounts", - "customer_id", - "payment_method", - "shipping_methods", - "shipping_total", - "discount_total", - "tax_total", - "subtotal", - "total", - "metadata", - ], - ["region"] - ) + const order = await orderService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, + }) res.json({ order }) } catch (error) { + console.log(error) throw error } } diff --git a/packages/medusa/src/api/routes/store/orders/index.js b/packages/medusa/src/api/routes/store/orders/index.js index eadb53a301..db8fa09032 100644 --- a/packages/medusa/src/api/routes/store/orders/index.js +++ b/packages/medusa/src/api/routes/store/orders/index.js @@ -8,7 +8,41 @@ export default app => { route.get("/:id", middlewares.wrap(require("./get-order").default)) - route.post("/", middlewares.wrap(require("./create-order").default)) + route.get( + "/cart/:cart_id", + middlewares.wrap(require("./get-order-by-cart").default) + ) return app } + +export const defaultRelations = [ + "shipping_address", + "items", + "items.variant", + "items.variant.product", + "shipping_methods", + "discounts", + "customer", + "payments", + "region", +] + +export const defaultFields = [ + "id", + "display_id", + "cart_id", + "customer_id", + "email", + "region_id", + "currency_code", + "tax_rate", + "created_at", + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "gift_card_total", + "subtotal", + "total", +] diff --git a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js index c5d7ea9ac2..5f04cbe1e2 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js @@ -1,5 +1,3 @@ -import mongoose from "mongoose" -import getProduct from "../get-product" import { request } from "../../../../../helpers/test-request" import { IdMap } from "medusa-test-utils" import { ProductServiceMock } from "../../../../../services/__mocks__/product" @@ -21,13 +19,13 @@ describe("Get product by id", () => { it("calls get product from productSerice", () => { expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(ProductServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("product1") + IdMap.getId("product1"), + { relations: ["images", "variants", "options"] } ) }) it("returns product decorated", () => { - expect(subject.body.product._id).toEqual(IdMap.getId("product1")) - expect(subject.body.product.decorated).toEqual(true) + expect(subject.body.product.id).toEqual(IdMap.getId("product1")) }) }) }) diff --git a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js index dc7d3b936f..04e13c7f22 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js @@ -16,12 +16,35 @@ describe("GET /store/products", () => { it("calls get product from productSerice", () => { expect(ProductServiceMock.list).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.list).toHaveBeenCalledWith({}) + expect(ProductServiceMock.list).toHaveBeenCalledWith( + {}, + { relations: ["variants", "options", "images"], skip: 0, take: 100 } + ) }) it("returns products", () => { - expect(subject.body.products[0]._id).toEqual(IdMap.getId("product1")) - expect(subject.body.products[1]._id).toEqual(IdMap.getId("product2")) + expect(subject.body.products[0].id).toEqual(IdMap.getId("product1")) + expect(subject.body.products[1].id).toEqual(IdMap.getId("product2")) + }) + }) + + describe("list all gift cards", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", "/store/products?is_giftcard=true") + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls list from productSerice", () => { + expect(ProductServiceMock.list).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.list).toHaveBeenCalledWith( + { is_giftcard: true }, + { relations: ["variants", "options", "images"], skip: 0, take: 100 } + ) }) }) }) diff --git a/packages/medusa/src/api/routes/store/products/get-product.js b/packages/medusa/src/api/routes/store/products/get-product.js index 9b59cd95ce..c458d1f0a5 100644 --- a/packages/medusa/src/api/routes/store/products/get-product.js +++ b/packages/medusa/src/api/routes/store/products/get-product.js @@ -1,32 +1,10 @@ -import { Validator } from "medusa-core-utils" - export default async (req, res) => { - const { productId } = req.params - - const schema = Validator.objectId() - const { value, error } = schema.validate(productId) - - if (error) { - throw error - } + const { id } = req.params const productService = req.scope.resolve("productService") - let product = await productService.retrieve(value) - - product = await productService.decorate( - product, - [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "variants", - "published", - ], - ["variants"] - ) + let product = await productService.retrieve(id, { + relations: ["images", "variants", "options"], + }) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/store/products/index.js b/packages/medusa/src/api/routes/store/products/index.js index 5682e533d7..8601550f3b 100644 --- a/packages/medusa/src/api/routes/store/products/index.js +++ b/packages/medusa/src/api/routes/store/products/index.js @@ -7,7 +7,7 @@ export default app => { app.use("/products", route) route.get("/", middlewares.wrap(require("./list-products").default)) - route.get("/:productId", middlewares.wrap(require("./get-product").default)) + route.get("/:id", middlewares.wrap(require("./get-product").default)) return app } diff --git a/packages/medusa/src/api/routes/store/products/list-products.js b/packages/medusa/src/api/routes/store/products/list-products.js index 38c6d47a7f..8645243187 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.js +++ b/packages/medusa/src/api/routes/store/products/list-products.js @@ -1,28 +1,26 @@ -import { Validator } from "medusa-core-utils" - export default async (req, res) => { - const selector = {} + try { + const productService = req.scope.resolve("productService") - const productService = req.scope.resolve("productService") - const products = await productService.list(selector) + const limit = parseInt(req.query.limit) || 100 + const offset = parseInt(req.query.offset) || 0 - const data = await Promise.all( - products.map(p => - productService.decorate( - p, - [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "variants", - "published", - ], - ["variants"] - ) - ) - ) - res.json({ products: data }) + const selector = {} + + if ("is_giftcard" in req.query && req.query.is_giftcard === "true") { + selector.is_giftcard = req.query.is_giftcard === "true" + } + + const listConfig = { + relations: ["variants", "options", "images"], + skip: offset, + take: limit, + } + + let products = await productService.list(selector, listConfig) + + res.json({ products, count: products.length, offset, limit }) + } catch (error) { + throw error + } } diff --git a/packages/medusa/src/api/routes/store/regions/__tests__/get-region.js b/packages/medusa/src/api/routes/store/regions/__tests__/get-region.js new file mode 100644 index 0000000000..3434718c65 --- /dev/null +++ b/packages/medusa/src/api/routes/store/regions/__tests__/get-region.js @@ -0,0 +1,37 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { RegionServiceMock } from "../../../../../services/__mocks__/region" + +describe("Get region by id", () => { + describe("get region by id successfull", () => { + let subject + beforeAll(async () => { + subject = await request( + "GET", + `/store/regions/${IdMap.getId("testRegion")}` + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls retrieve from region service", () => { + expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(RegionServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("testRegion"), + { + relations: [ + "countries", + "payment_providers", + "fulfillment_providers", + ], + } + ) + }) + + it("returns region", () => { + expect(subject.body.region.id).toEqual(IdMap.getId("testRegion")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/regions/__tests__/list-regions.js b/packages/medusa/src/api/routes/store/regions/__tests__/list-regions.js new file mode 100644 index 0000000000..935600f1d1 --- /dev/null +++ b/packages/medusa/src/api/routes/store/regions/__tests__/list-regions.js @@ -0,0 +1,36 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { RegionServiceMock } from "../../../../../services/__mocks__/region" + +describe("List regions", () => { + describe("list regions", () => { + let subject + beforeAll(async () => { + subject = await request("GET", `/store/regions`) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls list from region service", () => { + expect(RegionServiceMock.list).toHaveBeenCalledTimes(1) + expect(RegionServiceMock.list).toHaveBeenCalledWith( + {}, + { + relations: [ + "countries", + "payment_providers", + "fulfillment_providers", + ], + skip: 0, + take: 100, + } + ) + }) + + it("returns regions", () => { + expect(subject.body.regions[0].id).toEqual(IdMap.getId("testRegion")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/regions/get-region.js b/packages/medusa/src/api/routes/store/regions/get-region.js index c71c736d78..a6d314a0f9 100644 --- a/packages/medusa/src/api/routes/store/regions/get-region.js +++ b/packages/medusa/src/api/routes/store/regions/get-region.js @@ -1,26 +1,9 @@ -import { Validator } from "medusa-core-utils" - export default async (req, res) => { const { region_id } = req.params - - const schema = Validator.objectId() - const { value, error } = schema.validate(region_id) - - if (error) { - throw error - } - const regionService = req.scope.resolve("regionService") - const region = await regionService.retrieve(value) + const region = await regionService.retrieve(region_id, { + relations: ["countries", "payment_providers", "fulfillment_providers"], + }) - const data = await regionService.decorate(region, [ - "name", - "currency_code", - "tax_rate", - "countries", - "payment_providers", - "fulfillment_providers", - ]) - - res.json({ region: data }) + res.json({ region }) } diff --git a/packages/medusa/src/api/routes/store/regions/list-regions.js b/packages/medusa/src/api/routes/store/regions/list-regions.js index 5a3c12ee39..4fd39e1047 100644 --- a/packages/medusa/src/api/routes/store/regions/list-regions.js +++ b/packages/medusa/src/api/routes/store/regions/list-regions.js @@ -1,23 +1,18 @@ -import { Validator } from "medusa-core-utils" - export default async (req, res) => { + const regionService = req.scope.resolve("regionService") + + const limit = parseInt(req.query.limit) || 100 + const offset = parseInt(req.query.offset) || 0 + const selector = {} - const regionService = req.scope.resolve("regionService") - const regions = await regionService.list(selector) + const listConfig = { + relations: ["countries", "payment_providers", "fulfillment_providers"], + skip: offset, + take: limit, + } - const data = await Promise.all( - regions.map(r => - regionService.decorate(r, [ - "name", - "currency_code", - "tax_rate", - "countries", - "payment_providers", - "fulfillment_providers", - ]) - ) - ) + const regions = await regionService.list(selector, listConfig) - res.json({ regions: data }) + res.json({ regions }) } diff --git a/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-options.js b/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-options.js new file mode 100644 index 0000000000..32c8a806e7 --- /dev/null +++ b/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-options.js @@ -0,0 +1,40 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ShippingOptionServiceMock } from "../../../../../services/__mocks__/shipping-option" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("GET /store/shipping-options", () => { + describe("retrieves shipping options by product ids", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/store/shipping-options?product_ids=1,2,3®ion_id=test-region` + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls ShippingProfileService fetchOptionsByProductIds", () => { + expect(ProductServiceMock.list).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.list).toHaveBeenCalledWith({ + id: ["1", "2", "3"], + }) + expect(ShippingOptionServiceMock.list).toHaveBeenCalledTimes(1) + expect(ShippingOptionServiceMock.list).toHaveBeenCalledWith( + { + profile_id: [undefined, undefined], + region_id: "test-region", + }, + { relations: ["requirements"] } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js b/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js index da2686f982..256f0d461d 100644 --- a/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js +++ b/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js @@ -21,7 +21,16 @@ describe("GET /store/shipping-options", () => { it("calls CartService retrieve", () => { expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(CartServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("emptyCart") + IdMap.getId("emptyCart"), + { + select: ["subtotal"], + relations: [ + "region", + "items", + "items.variant", + "items.variant.product", + ], + } ) }) @@ -38,8 +47,8 @@ describe("GET /store/shipping-options", () => { expect(subject.status).toEqual(200) }) - it("returns the cart", () => { - expect(subject.body.shipping_options[0]._id).toEqual( + it("returns the shippingOptions", () => { + expect(subject.body.shipping_options[0].id).toEqual( IdMap.getId("cartShippingOption") ) }) diff --git a/packages/medusa/src/api/routes/store/shipping-options/list-options.js b/packages/medusa/src/api/routes/store/shipping-options/list-options.js index a785cdbc00..704abda25e 100644 --- a/packages/medusa/src/api/routes/store/shipping-options/list-options.js +++ b/packages/medusa/src/api/routes/store/shipping-options/list-options.js @@ -1,19 +1,26 @@ -import { Validator, MedusaError } from "medusa-core-utils" - export default async (req, res) => { const productIds = (req.query.product_ids && req.query.product_ids.split(",")) || [] const regionId = req.query.region_id try { - const shippingProfileService = req.scope.resolve("shippingProfileService") + const productService = req.scope.resolve("productService") + const shippingOptionService = req.scope.resolve("shippingOptionService") - const options = await shippingProfileService.fetchOptionsByProductIds( - productIds, - { - region_id: regionId, - } - ) + const query = {} + + if (regionId) { + query.region_id = regionId + } + + if (productIds.length) { + const prods = await productService.list({ id: productIds }) + query.profile_id = prods.map(p => p.profile_id) + } + + const options = await shippingOptionService.list(query, { + relations: ["requirements"], + }) res.status(200).json({ shipping_options: options }) } catch (err) { diff --git a/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.js b/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.js index 193fdf66de..a882ba91a3 100644 --- a/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.js +++ b/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.js @@ -14,7 +14,11 @@ export default async (req, res) => { const cartService = req.scope.resolve("cartService") const shippingProfileService = req.scope.resolve("shippingProfileService") - const cart = await cartService.retrieve(value.cart_id) + const cart = await cartService.retrieve(value.cart_id, { + select: ["subtotal"], + relations: ["region", "items", "items.variant", "items.variant.product"], + }) + const options = await shippingProfileService.fetchCartOptions(cart) res.status(200).json({ shipping_options: options }) diff --git a/packages/medusa/src/api/routes/store/swaps/create-swap.js b/packages/medusa/src/api/routes/store/swaps/create-swap.js index 4453552f3e..370f96f65f 100644 --- a/packages/medusa/src/api/routes/store/swaps/create-swap.js +++ b/packages/medusa/src/api/routes/store/swaps/create-swap.js @@ -15,7 +15,7 @@ export default async (req, res) => { try { const swap = await swapService.retrieveByCartId(value.cart_id) const data = await swapService.registerCartCompletion( - swap._id, + swap.id, value.cart_id ) diff --git a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js new file mode 100644 index 0000000000..8d90d68aa6 --- /dev/null +++ b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js @@ -0,0 +1,26 @@ +import { request } from "../../../../../helpers/test-request" +import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" + +describe("Get variant by id", () => { + describe("get variant by id successfull", () => { + let subject + beforeAll(async () => { + subject = await request("GET", `/store/variants/1`) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls get variant from variantSerice", () => { + expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledWith("1", { + relations: ["prices"], + }) + }) + + it("returns variant decorated", () => { + expect(subject.body.variant.id).toEqual("1") + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js b/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js new file mode 100644 index 0000000000..eb24500219 --- /dev/null +++ b/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js @@ -0,0 +1,21 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" + +describe("List variants", () => { + describe("list variants successfull", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/store/variants`) + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("returns variants", () => { + expect(subject.body.variants[0].id).toEqual(IdMap.getId("testVariant")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/variants/get-variant.js b/packages/medusa/src/api/routes/store/variants/get-variant.js index b9f609353e..733e497ed6 100644 --- a/packages/medusa/src/api/routes/store/variants/get-variant.js +++ b/packages/medusa/src/api/routes/store/variants/get-variant.js @@ -1,31 +1,11 @@ -import { Validator } from "medusa-core-utils" - export default async (req, res) => { - const { variant_id } = req.params + const { id } = req.params - const schema = Validator.objectId() - const { value, error } = schema.validate(variant_id) - - if (error) { + try { + const variantService = req.scope.resolve("productVariantService") + let variant = await variantService.retrieve(id, { relations: ["prices"] }) + res.json({ variant }) + } catch (error) { throw error } - - const variantService = req.scope.resolve("productVariantService") - let variant = await variantService.retrieve(value) - - let includeFields = [ - "title", - "prices", - "sku", - "ean", - "image", - "inventory_quantity", - "allow_backorder", - "manage_inventory", - ] - if ("fields" in req.query) { - includeFields = req.query.fields.split(",") - } - variant = await variantService.decorate(variant, includeFields) - res.json({ variant }) } diff --git a/packages/medusa/src/api/routes/store/variants/index.js b/packages/medusa/src/api/routes/store/variants/index.js index 7d728c7946..00c697e0f1 100644 --- a/packages/medusa/src/api/routes/store/variants/index.js +++ b/packages/medusa/src/api/routes/store/variants/index.js @@ -7,7 +7,7 @@ export default app => { app.use("/variants", route) route.get("/", middlewares.wrap(require("./list-variants").default)) - route.get("/:variant_id", middlewares.wrap(require("./get-variant").default)) + route.get("/:id", middlewares.wrap(require("./get-variant").default)) return app } diff --git a/packages/medusa/src/api/routes/store/variants/list-variants.js b/packages/medusa/src/api/routes/store/variants/list-variants.js index 8655de9cdf..cdb9461c12 100644 --- a/packages/medusa/src/api/routes/store/variants/list-variants.js +++ b/packages/medusa/src/api/routes/store/variants/list-variants.js @@ -1,31 +1,21 @@ -import { Validator } from "medusa-core-utils" - export default async (req, res) => { - const selector = {} + const limit = parseInt(req.query.limit) || 100 + const offset = parseInt(req.query.offset) || 0 + + let selector = {} + + const listConfig = { + relations: [], + skip: offset, + take: limit, + } if ("ids" in req.query) { - selector["_id"] = { $in: req.query.ids.split(",") } + selector = { id: req.query.ids.split(",") } } const variantService = req.scope.resolve("productVariantService") - const variants = await variantService.list(selector) + const variants = await variantService.list(selector, listConfig) - let includeFields = [ - "title", - "prices", - "sku", - "ean", - "image", - "inventory_quantity", - "allow_backorder", - "manage_inventory", - ] - if ("fields" in req.query) { - includeFields = req.query.fields.split(",") - } - - const data = await Promise.all( - variants.map(v => variantService.decorate(v, includeFields)) - ) - res.json({ variants: data }) + res.json({ variants }) } diff --git a/packages/medusa/src/app.js b/packages/medusa/src/app.js index ccf5efae6b..8be0d664af 100644 --- a/packages/medusa/src/app.js +++ b/packages/medusa/src/app.js @@ -1,5 +1,6 @@ import "core-js/stable" import "regenerator-runtime/runtime" +import "reflect-metadata" import express from "express" import loaders from "./loaders" import Logger from "./loaders/logger" diff --git a/packages/medusa/src/commands/migrate.js b/packages/medusa/src/commands/migrate.js new file mode 100644 index 0000000000..dc815def86 --- /dev/null +++ b/packages/medusa/src/commands/migrate.js @@ -0,0 +1,155 @@ +import { createConnection, MigrationExecutor } from "typeorm" +import { spawn, execSync } from "child_process" +import chokidar from "chokidar" +import path from "path" +import fs from "fs" +import _ from "lodash" +import { getConfigFile, createRequireFromPath } from "medusa-core-utils" +import { sync as existsSync } from "fs-exists-cached" + +import loaders from "../loaders" +import Logger from "../loaders/logger" + +function createFileContentHash(path, files) { + return path + files +} + +// TODO: Create unique id for each plugin +function createPluginId(name) { + return name +} + +/** + * Finds the correct path for the plugin. If it is a local plugin it will be + * found in the plugins folder. Otherwise we will look for the plugin in the + * installed npm packages. + * @param {string} pluginName - the name of the plugin to find. Should match + * the name of the folder where the plugin is contained. + * @return {object} the plugin details + */ +function resolvePlugin(pluginName) { + // Only find plugins when we're not given an absolute path + if (!existsSync(pluginName)) { + // Find the plugin in the local plugins folder + const resolvedPath = path.resolve(`./plugins/${pluginName}`) + + if (existsSync(resolvedPath)) { + if (existsSync(`${resolvedPath}/package.json`)) { + const packageJSON = JSON.parse( + fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`) + ) + const name = packageJSON.name || pluginName + //warnOnIncompatiblePeerDependency(name, packageJSON) + + return { + resolve: resolvedPath, + name, + id: createPluginId(name), + options: {}, + version: + packageJSON.version || createFileContentHash(resolvedPath, `**`), + } + } else { + // Make package.json a requirement for local plugins too + throw new Error(`Plugin ${pluginName} requires a package.json file`) + } + } + } + + const rootDir = path.resolve(".") + + /** + * Here we have an absolute path to an internal plugin, or a name of a module + * which should be located in node_modules. + */ + try { + const requireSource = + rootDir !== null + ? createRequireFromPath(`${rootDir}/:internal:`) + : require + + // If the path is absolute, resolve the directory of the internal plugin, + // otherwise resolve the directory containing the package.json + const resolvedPath = path.dirname( + requireSource.resolve(`${pluginName}/package.json`) + ) + + const packageJSON = JSON.parse( + fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`) + ) + // warnOnIncompatiblePeerDependency(packageJSON.name, packageJSON) + + return { + resolve: resolvedPath, + id: createPluginId(packageJSON.name), + name: packageJSON.name, + version: packageJSON.version, + } + } catch (err) { + throw new Error( + `Unable to find plugin "${pluginName}". Perhaps you need to install its package?` + ) + } +} + +const t = async function({ port, directory }) { + const args = process.argv + args.shift() + args.shift() + args.shift() + + const { configModule } = getConfigFile(directory, `medusa-config`) + const { plugins } = configModule + + const resolved = plugins.map(plugin => { + if (_.isString(plugin)) { + return resolvePlugin(plugin) + } + + const details = resolvePlugin(plugin.resolve) + details.options = plugin.options + + return details + }) + + resolved.push({ + resolve: `${directory}/dist`, + name: `project-plugin`, + id: createPluginId(`project-plugin`), + options: {}, + version: createFileContentHash(process.cwd(), `**`), + }) + + const migrationDirs = [] + const coreMigrations = path.resolve(__dirname, "../migrations") + + migrationDirs.push(`${coreMigrations}/*.js`) + + for (const p of resolved) { + const exists = existsSync(`${p.resolve}/migrations`) + if (exists) { + migrationDirs.push(`${p.resolve}/migrations/*.js`) + } + } + + const connection = await createConnection({ + type: configModule.projectConfig.database_type, + url: configModule.projectConfig.database_url, + extra: configModule.projectConfig.database_extra || {}, + migrations: migrationDirs, + logging: true, + }) + + if (args[0] === "run") { + await connection.runMigrations() + await connection.close() + Logger.info("Migrations completed.") + process.exit() + } else if (args[0] === "show") { + const unapplied = await connection.showMigrations() + await connection.close() + process.exit(unapplied ? 1 : 0) + } +} + +export default t diff --git a/packages/medusa/src/helpers/test-request.js b/packages/medusa/src/helpers/test-request.js index 14992a95f8..d7e7980979 100644 --- a/packages/medusa/src/helpers/test-request.js +++ b/packages/medusa/src/helpers/test-request.js @@ -1,3 +1,4 @@ +import { MockManager } from "medusa-test-utils" import { createContainer, asValue } from "awilix" import express from "express" import cookieParser from "cookie-parser" @@ -28,6 +29,7 @@ container.register({ logger: asValue({ error: () => {}, }), + manager: asValue(MockManager), }) testApp.set("trust proxy", 1) diff --git a/packages/medusa/src/loaders/database.js b/packages/medusa/src/loaders/database.js new file mode 100644 index 0000000000..ca04478e84 --- /dev/null +++ b/packages/medusa/src/loaders/database.js @@ -0,0 +1,18 @@ +import { createConnection, Connection, DefaultNamingStrategy } from "typeorm" + +import { ShortenedNamingStrategy } from "../utils/naming-strategy" + +export default async ({ container, configModule }) => { + const entities = container.resolve("db_entities") + + const connection = await createConnection({ + type: configModule.projectConfig.database_type, + url: configModule.projectConfig.database_url, + extra: configModule.projectConfig.database_extra || {}, + entities, + namingStrategy: new ShortenedNamingStrategy(), + logging: configModule.projectConfig.database_logging || false, + }) + + return connection +} diff --git a/packages/medusa/src/loaders/defaults.js b/packages/medusa/src/loaders/defaults.js index 4dcd590880..946588c7d7 100644 --- a/packages/medusa/src/loaders/defaults.js +++ b/packages/medusa/src/loaders/defaults.js @@ -1,30 +1,25 @@ export default async ({ container }) => { - const counterService = container.resolve("counterService") const storeService = container.resolve("storeService") const profileService = container.resolve("shippingProfileService") - let payIds - try { + const entityManager = container.resolve("manager") + + await entityManager.transaction(async manager => { + await storeService.withTransaction(manager).create() + + let payIds + const pProviderService = container.resolve("paymentProviderService") const payProviders = container.resolve("paymentProviders") payIds = payProviders.map(p => p.getIdentifier()) - } catch (e) { - payIds = [] - } + await pProviderService.registerInstalledProviders(payIds) - let fulfilIds - try { + let fulfilIds + const fProviderService = container.resolve("fulfillmentProviderService") const fulfilProviders = container.resolve("fulfillmentProviders") fulfilIds = fulfilProviders.map(p => p.getIdentifier()) - } catch (e) { - fulfilIds = [] - } + await fProviderService.registerInstalledProviders(fulfilIds) - await storeService.create({ - fulfillment_providers: fulfilIds, - payment_providers: payIds, + await profileService.withTransaction(manager).createDefault() + await profileService.withTransaction(manager).createGiftCardDefault() }) - - await counterService.createDefaults() - await profileService.createDefault() - await profileService.createGiftCardDefault() } diff --git a/packages/medusa/src/loaders/index.js b/packages/medusa/src/loaders/index.js index 8cbbc703de..842864ded0 100644 --- a/packages/medusa/src/loaders/index.js +++ b/packages/medusa/src/loaders/index.js @@ -1,9 +1,11 @@ import { createContainer, asValue } from "awilix" import Redis from "ioredis" import { getConfigFile } from "medusa-core-utils" +import requestIp from "request-ip" import expressLoader from "./express" -import mongooseLoader from "./mongoose" +import databaseLoader from "./database" +import repositoriesLoader from "./repositories" import apiLoader from "./api" import modelsLoader from "./models" import servicesLoader from "./services" @@ -12,6 +14,7 @@ import passportLoader from "./passport" import pluginsLoader from "./plugins" import defaultsLoader from "./defaults" import Logger from "./logger" +import { getManager } from "typeorm" export default async ({ directory: rootDirectory, expressApp }) => { const { configModule, configFilePath } = getConfigFile( @@ -40,6 +43,18 @@ export default async ({ directory: rootDirectory, expressApp }) => { const client = new Redis(configModule.projectConfig.redis_url) const subscriber = new Redis(configModule.projectConfig.redis_url) + // Add additional information to context of request + expressApp.use((req, res, next) => { + const ipAddress = requestIp.getClientIp(req) + + const context = { + ip_address: ipAddress, + } + + req.request_context = context + next() + }) + container.register({ redisClient: asValue(client), redisSubscriber: asValue(subscriber), @@ -49,15 +64,22 @@ export default async ({ directory: rootDirectory, expressApp }) => { await modelsLoader({ container }) Logger.info("Models initialized") + await repositoriesLoader({ container }) + Logger.info("Repositories initialized") + + const dbConnection = await databaseLoader({ container, configModule }) + Logger.info("Database initialized") + + container.register({ + manager: asValue(dbConnection.manager), + }) + await servicesLoader({ container, configModule }) Logger.info("Services initialized") await subscribersLoader({ container }) Logger.info("Subscribers initialized") - const dbConnection = await mongooseLoader({ container, configModule }) - Logger.info("MongoDB Intialized") - await expressLoader({ app: expressApp, configModule }) Logger.info("Express Intialized") @@ -66,6 +88,9 @@ export default async ({ directory: rootDirectory, expressApp }) => { // Add the registered services to the request scope expressApp.use((req, res, next) => { + container.register({ + manager: asValue(getManager()), + }) req.scope = container.createScope() next() }) diff --git a/packages/medusa/src/loaders/models.js b/packages/medusa/src/loaders/models.js index e19ab25495..687eecb215 100644 --- a/packages/medusa/src/loaders/models.js +++ b/packages/medusa/src/loaders/models.js @@ -1,8 +1,8 @@ -import { BaseModel } from "medusa-interfaces" -import { Lifetime } from "awilix" import glob from "glob" import path from "path" -import { asFunction } from "awilix" +import { BaseModel } from "medusa-interfaces" +import { EntitySchema } from "typeorm" +import { Lifetime, asClass, asValue } from "awilix" /** * Registers all models in the model directory @@ -13,10 +13,17 @@ export default ({ container }) => { const core = glob.sync(coreFull, { cwd: __dirname }) core.forEach(fn => { - const loaded = require(fn).default - const name = formatRegistrationName(fn) - container.register({ - [name]: asFunction(cradle => new loaded(cradle)).singleton(), + const loaded = require(fn) + + Object.entries(loaded).map(([key, val]) => { + if (typeof val === "function" || val instanceof EntitySchema) { + const name = formatRegistrationName(fn) + container.register({ + [name]: asClass(val), + }) + + container.registerAdd("db_entities", asValue(val)) + } }) }) } diff --git a/packages/medusa/src/loaders/mongoose.js b/packages/medusa/src/loaders/mongoose.js deleted file mode 100644 index 228f61b86f..0000000000 --- a/packages/medusa/src/loaders/mongoose.js +++ /dev/null @@ -1,20 +0,0 @@ -import mongoose from "mongoose" - -export default async ({ container, configModule }) => { - const logger = container.resolve("logger") - - mongoose.connection.on("error", err => { - logger.error(err) - }) - - return mongoose - .connect(configModule.projectConfig.mongo_url, { - useNewUrlParser: true, - useCreateIndex: true, - useUnifiedTopology: true, - useFindAndModify: false, - }) - .catch(err => { - logger.error(err) - }) -} diff --git a/packages/medusa/src/loaders/repositories.js b/packages/medusa/src/loaders/repositories.js new file mode 100644 index 0000000000..c66507c144 --- /dev/null +++ b/packages/medusa/src/loaders/repositories.js @@ -0,0 +1,45 @@ +import glob from "glob" +import path from "path" +import { Lifetime, asClass, asValue } from "awilix" + +/** + * Registers all models in the model directory + */ +export default ({ container }) => { + let corePath = "../repositories/*.js" + const coreFull = path.join(__dirname, corePath) + + const core = glob.sync(coreFull, { cwd: __dirname }) + core.forEach(fn => { + const loaded = require(fn) + + Object.entries(loaded).map(([key, val]) => { + if (typeof val === "function") { + const name = formatRegistrationName(fn) + container.register({ + [name]: asClass(val), + }) + } + }) + }) +} + +function formatRegistrationName(fn) { + const offset = process.env.NODE_ENV === "test" ? 3 : 2 + + const descriptorIndex = fn.split(".").length - 2 + const descriptor = fn.split(".")[descriptorIndex] + const splat = descriptor.split("/") + const rawname = splat[splat.length - 1] + const namespace = splat[splat.length - offset] + const upperNamespace = "Repository" + // namespace.charAt(0).toUpperCase() + namespace.slice(1, -1) + + const parts = rawname.split("-").map((n, index) => { + if (index !== 0) { + return n.charAt(0).toUpperCase() + n.slice(1) + } + return n + }) + return parts.join("") + upperNamespace +} diff --git a/packages/medusa/src/migrations/1611063162649-initial_schema.ts b/packages/medusa/src/migrations/1611063162649-initial_schema.ts new file mode 100644 index 0000000000..55be851445 --- /dev/null +++ b/packages/medusa/src/migrations/1611063162649-initial_schema.ts @@ -0,0 +1,382 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class initialSchema1611063162649 implements MigrationInterface { + name = 'initialSchema1611063162649' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "fulfillment_provider" ("id" character varying NOT NULL, "is_installed" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_beb35a6de60a6c4f91d5ae57e44" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "fulfillment_item" ("fulfillment_id" character varying NOT NULL, "item_id" character varying NOT NULL, "quantity" integer NOT NULL, CONSTRAINT "PK_bc3e8a388de75db146a249922e0" PRIMARY KEY ("fulfillment_id", "item_id"))`); + await queryRunner.query(`CREATE TABLE "fulfillment" ("id" character varying NOT NULL, "swap_id" character varying, "order_id" character varying, "tracking_numbers" jsonb NOT NULL DEFAULT '[]', "data" jsonb NOT NULL, "shipped_at" TIMESTAMP WITH TIME ZONE, "canceled_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "idempotency_key" character varying, "provider_id" character varying, CONSTRAINT "PK_50c102da132afffae660585981f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "return_item" ("return_id" character varying NOT NULL, "item_id" character varying NOT NULL, "quantity" integer NOT NULL, "is_requested" boolean NOT NULL DEFAULT true, "requested_quantity" integer, "received_quantity" integer, "metadata" jsonb, CONSTRAINT "PK_46409dc1dd5f38509b9000c3069" PRIMARY KEY ("return_id", "item_id"))`); + await queryRunner.query(`CREATE TABLE "currency" ("code" character varying NOT NULL, "symbol" character varying NOT NULL, "symbol_native" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_723472e41cae44beb0763f4039c" PRIMARY KEY ("code"))`); + await queryRunner.query(`CREATE TABLE "country" ("id" SERIAL NOT NULL, "iso_2" character varying NOT NULL, "iso_3" character varying NOT NULL, "num_code" integer NOT NULL, "name" character varying NOT NULL, "display_name" character varying NOT NULL, "region_id" character varying, CONSTRAINT "PK_bf6e37c231c4f4ea56dcd887269" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e78901b1131eaf8203d9b1cb5f" ON "country" ("iso_2") `); + await queryRunner.query(`CREATE TABLE "payment_provider" ("id" character varying NOT NULL, "is_installed" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_ea94f42b6c88e9191c3649d7522" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "region" ("id" character varying NOT NULL, "name" character varying NOT NULL, "currency_code" character varying NOT NULL, "tax_rate" numeric NOT NULL, "tax_code" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_5f48ffc3af96bc486f5f3f3a6da" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "image" ("id" character varying NOT NULL, "url" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_d6db1ab4ee9ad9dbe86c64e4cc3" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "money_amount" ("id" character varying NOT NULL, "currency_code" character varying NOT NULL, "amount" integer NOT NULL, "sale_amount" integer DEFAULT null, "variant_id" character varying, "region_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_022e49a7e21a8dfb820f788778a" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "product_variant" ("id" character varying NOT NULL, "title" character varying NOT NULL, "product_id" character varying NOT NULL, "sku" character varying, "barcode" character varying, "ean" character varying, "upc" character varying, "inventory_quantity" integer NOT NULL, "allow_backorder" boolean NOT NULL DEFAULT false, "manage_inventory" boolean NOT NULL DEFAULT true, "hs_code" character varying, "origin_country" character varying, "mid_code" character varying, "material" character varying, "weight" integer, "length" integer, "height" integer, "width" integer, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_1ab69c9935c61f7c70791ae0a9f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f4dc2c0888b66d547c175f090e" ON "product_variant" ("sku") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9db95c4b71f632fc93ecbc3d8b" ON "product_variant" ("barcode") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7124082c8846a06a857cca386c" ON "product_variant" ("ean") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0a3f124dc5b167622217fee02" ON "product_variant" ("upc") `); + await queryRunner.query(`CREATE TABLE "product_option_value" ("id" character varying NOT NULL, "value" character varying NOT NULL, "option_id" character varying NOT NULL, "variant_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_2ab71ed3b21be5800905c621535" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_cdf4388f294b30a25c627d69fe" ON "product_option_value" ("option_id") `); + await queryRunner.query(`CREATE TABLE "product_option" ("id" character varying NOT NULL, "title" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, "product_id" character varying, CONSTRAINT "PK_4cf3c467e9bc764bdd32c4cd938" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "shipping_option_requirement_type_enum" AS ENUM('min_subtotal', 'max_subtotal')`); + await queryRunner.query(`CREATE TABLE "shipping_option_requirement" ("id" character varying NOT NULL, "shipping_option_id" character varying NOT NULL, "type" "shipping_option_requirement_type_enum" NOT NULL, "amount" integer NOT NULL, CONSTRAINT "PK_a0ff15442606d9f783602cb23a7" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "shipping_option_price_type_enum" AS ENUM('flat_rate', 'calculated')`); + await queryRunner.query(`CREATE TABLE "shipping_option" ("id" character varying NOT NULL, "name" character varying NOT NULL, "region_id" character varying NOT NULL, "profile_id" character varying NOT NULL, "provider_id" character varying NOT NULL, "price_type" "shipping_option_price_type_enum" NOT NULL, "amount" integer, "is_return" boolean NOT NULL DEFAULT false, "data" jsonb NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "CHK_7a367f5901ae0a5b0df75aee38" CHECK ("amount" >= 0), CONSTRAINT "PK_2e56fddaa65f3a26d402e5d786e" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "shipping_profile_type_enum" AS ENUM('default', 'gift_card', 'custom')`); + await queryRunner.query(`CREATE TABLE "shipping_profile" ("id" character varying NOT NULL, "name" character varying NOT NULL, "type" "shipping_profile_type_enum" NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_c8120e4543a5a3a121f2968a1ec" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "product" ("id" character varying NOT NULL, "title" character varying NOT NULL, "subtitle" character varying, "description" character varying, "tags" character varying, "handle" character varying, "is_giftcard" boolean NOT NULL DEFAULT false, "thumbnail" character varying, "profile_id" character varying NOT NULL, "weight" integer, "length" integer, "height" integer, "width" integer, "hs_code" character varying, "origin_country" character varying, "mid_code" character varying, "material" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_bebc9158e480b949565b4dc7a82" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_db7355f7bd36c547c8a4f539e5" ON "product" ("handle") `); + await queryRunner.query(`CREATE TYPE "discount_rule_type_enum" AS ENUM('fixed', 'percentage', 'free_shipping')`); + await queryRunner.query(`CREATE TYPE "discount_rule_allocation_enum" AS ENUM('total', 'item')`); + await queryRunner.query(`CREATE TABLE "discount_rule" ("id" character varying NOT NULL, "description" character varying NOT NULL, "type" "discount_rule_type_enum" NOT NULL, "value" integer NOT NULL, "allocation" "discount_rule_allocation_enum", "usage_limit" integer, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_ac2c280de3701b2d66f6817f760" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "discount" ("id" character varying NOT NULL, "code" character varying NOT NULL, "is_dynamic" boolean NOT NULL, "rule_id" character varying, "is_disabled" boolean NOT NULL, "parent_discount_id" character varying, "starts_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, "ends_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_d05d8712e429673e459e7f1cddb" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_087926f6fec32903be3c8eedfa" ON "discount" ("code") `); + await queryRunner.query(`CREATE TYPE "payment_session_status_enum" AS ENUM('authorized', 'pending', 'requires_more', 'error', 'canceled')`); + await queryRunner.query(`CREATE TABLE "payment_session" ("id" character varying NOT NULL, "cart_id" character varying NOT NULL, "provider_id" character varying NOT NULL, "is_selected" boolean, "status" "payment_session_status_enum" NOT NULL, "data" jsonb NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "idempotency_key" character varying, CONSTRAINT "OneSelected" UNIQUE ("cart_id", "is_selected"), CONSTRAINT "PK_a1a91b20f7f3b1e5afb5485cbcd" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "payment" ("id" character varying NOT NULL, "swap_id" character varying, "cart_id" character varying, "order_id" character varying, "amount" integer NOT NULL, "currency_code" character varying NOT NULL, "amount_refunded" integer NOT NULL DEFAULT '0', "provider_id" character varying NOT NULL, "data" jsonb NOT NULL, "captured_at" TIMESTAMP WITH TIME ZONE, "canceled_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "REL_c17aff091441b7c25ec3d68d36" UNIQUE ("swap_id"), CONSTRAINT "REL_4665f17abc1e81dd58330e5854" UNIQUE ("cart_id"), CONSTRAINT "PK_fcaec7df5adf9cac408c686b2ab" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "gift_card" ("id" character varying NOT NULL, "code" character varying NOT NULL, "value" integer NOT NULL, "balance" integer NOT NULL, "region_id" character varying NOT NULL, "order_id" character varying, "is_disabled" boolean NOT NULL DEFAULT false, "ends_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "REL_dfc1f02bb0552e79076aa58dbb" UNIQUE ("order_id"), CONSTRAINT "PK_af4e338d2d41035042843ad641f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_53cb5605fa42e82b4d47b47bda" ON "gift_card" ("code") `); + await queryRunner.query(`CREATE TYPE "cart_type_enum" AS ENUM('default', 'swap', 'payment_link')`); + await queryRunner.query(`CREATE TABLE "cart" ("id" character varying NOT NULL, "email" character varying, "billing_address_id" character varying, "shipping_address_id" character varying, "region_id" character varying NOT NULL, "customer_id" character varying, "payment_id" character varying, "type" "cart_type_enum" NOT NULL DEFAULT 'default', "completed_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "REL_9d1a161434c610aae7c3df2dc7" UNIQUE ("payment_id"), CONSTRAINT "PK_c524ec48751b9b5bcfbf6e59be7" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "shipping_method" ("id" character varying NOT NULL, "shipping_option_id" character varying NOT NULL, "order_id" character varying, "cart_id" character varying, "swap_id" character varying, "return_id" character varying, "price" integer NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "REL_1d9ad62038998c3a85c77a53cf" UNIQUE ("return_id"), CONSTRAINT "CHK_64c6812fe7815be30d688df513" CHECK ("price" >= 0), CONSTRAINT "CHK_3c00b878c1426d119cd70aa065" CHECK ("order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL), CONSTRAINT "PK_b9b0adfad3c6b99229c1e7d4865" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_fc963e94854bff2714ca84cd19" ON "shipping_method" ("shipping_option_id") `); + await queryRunner.query(`CREATE INDEX "IDX_5267705a43d547e232535b656c" ON "shipping_method" ("order_id") `); + await queryRunner.query(`CREATE INDEX "IDX_d92993a7d554d84571f4eea1d1" ON "shipping_method" ("cart_id") `); + await queryRunner.query(`CREATE INDEX "IDX_fb94fa8d5ca940daa2a58139f8" ON "shipping_method" ("swap_id") `); + await queryRunner.query(`CREATE INDEX "IDX_1d9ad62038998c3a85c77a53cf" ON "shipping_method" ("return_id") `); + await queryRunner.query(`CREATE TYPE "return_status_enum" AS ENUM('requested', 'received', 'requires_action')`); + await queryRunner.query(`CREATE TABLE "return" ("id" character varying NOT NULL, "status" "return_status_enum" NOT NULL DEFAULT 'requested', "swap_id" character varying, "order_id" character varying, "shipping_data" jsonb, "refund_amount" integer NOT NULL, "received_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "REL_bad82d7bff2b08b87094bfac3d" UNIQUE ("swap_id"), CONSTRAINT "PK_c8ad68d13e76d75d803b5aeebc4" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "swap_fulfillment_status_enum" AS ENUM('not_fulfilled', 'fulfilled', 'shipped', 'canceled', 'requires_action')`); + await queryRunner.query(`CREATE TYPE "swap_payment_status_enum" AS ENUM('not_paid', 'awaiting', 'captured', 'canceled', 'difference_refunded', 'partially_refunded', 'refunded', 'requires_action')`); + await queryRunner.query(`CREATE TABLE "swap" ("id" character varying NOT NULL, "fulfillment_status" "swap_fulfillment_status_enum" NOT NULL, "payment_status" "swap_payment_status_enum" NOT NULL, "order_id" character varying NOT NULL, "difference_due" integer, "shipping_address_id" character varying, "cart_id" character varying, "confirmed_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "REL_402e8182bc553e082f6380020b" UNIQUE ("cart_id"), CONSTRAINT "PK_4a10d0f359339acef77e7f986d9" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "line_item" ("id" character varying NOT NULL, "cart_id" character varying, "order_id" character varying, "swap_id" character varying, "title" character varying NOT NULL, "description" character varying, "thumbnail" character varying, "is_giftcard" boolean NOT NULL DEFAULT false, "should_merge" boolean NOT NULL DEFAULT true, "allow_discounts" boolean NOT NULL DEFAULT true, "has_shipping" boolean, "unit_price" integer NOT NULL, "variant_id" character varying, "quantity" integer NOT NULL, "fulfilled_quantity" integer, "returned_quantity" integer, "shipped_quantity" integer, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "CHK_64eef00a5064887634f1680866" CHECK ("quantity" > 0), CONSTRAINT "CHK_91f40396d847f6ecfd9f752bf8" CHECK ("returned_quantity" <= "quantity"), CONSTRAINT "CHK_0cd85e15610d11b553d5e8fda6" CHECK ("shipped_quantity" <= "fulfilled_quantity"), CONSTRAINT "CHK_c61716c68f5ad5de2834c827d3" CHECK ("fulfilled_quantity" <= "quantity"), CONSTRAINT "PK_cce6b13e67fa506d1d9618ac68b" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_27283ee631862266d0f1c68064" ON "line_item" ("cart_id") `); + await queryRunner.query(`CREATE INDEX "IDX_43a2b24495fe1d9fc2a9c835bc" ON "line_item" ("order_id") `); + await queryRunner.query(`CREATE INDEX "IDX_3fa354d8d1233ff81097b2fcb6" ON "line_item" ("swap_id") `); + await queryRunner.query(`CREATE INDEX "IDX_5371cbaa3be5200f373d24e3d5" ON "line_item" ("variant_id") `); + await queryRunner.query(`CREATE TABLE "gift_card_transaction" ("id" character varying NOT NULL, "gift_card_id" character varying NOT NULL, "order_id" character varying NOT NULL, "amount" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "gcuniq" UNIQUE ("gift_card_id", "order_id"), CONSTRAINT "PK_cfb5b4ba5447a507aef87d73fe7" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "refund_reason_enum" AS ENUM('discount', 'return', 'swap', 'other')`); + await queryRunner.query(`CREATE TABLE "refund" ("id" character varying NOT NULL, "order_id" character varying NOT NULL, "amount" integer NOT NULL, "note" character varying, "reason" "refund_reason_enum" NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "PK_f1cefa2e60d99b206c46c1116e5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "order_status_enum" AS ENUM('pending', 'completed', 'archived', 'canceled', 'requires_action')`); + await queryRunner.query(`CREATE TYPE "order_fulfillment_status_enum" AS ENUM('not_fulfilled', 'partially_fulfilled', 'fulfilled', 'partially_shipped', 'shipped', 'partially_returned', 'returned', 'canceled', 'requires_action')`); + await queryRunner.query(`CREATE TYPE "order_payment_status_enum" AS ENUM('not_paid', 'awaiting', 'captured', 'partially_refunded', 'refunded', 'canceled', 'requires_action')`); + await queryRunner.query(`CREATE TABLE "order" ("id" character varying NOT NULL, "status" "order_status_enum" NOT NULL DEFAULT 'pending', "fulfillment_status" "order_fulfillment_status_enum" NOT NULL DEFAULT 'not_fulfilled', "payment_status" "order_payment_status_enum" NOT NULL DEFAULT 'not_paid', "display_id" SERIAL NOT NULL, "cart_id" character varying, "customer_id" character varying NOT NULL, "email" character varying NOT NULL, "billing_address_id" character varying, "shipping_address_id" character varying, "region_id" character varying NOT NULL, "currency_code" character varying NOT NULL, "tax_rate" integer NOT NULL, "canceled_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "REL_c99a206eb11ad45f6b7f04f2dc" UNIQUE ("cart_id"), CONSTRAINT "PK_1031171c13130102495201e3e20" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "customer" ("id" character varying NOT NULL, "email" character varying NOT NULL, "first_name" character varying, "last_name" character varying, "billing_address_id" character varying, "password_hash" character varying, "phone" character varying, "has_account" boolean NOT NULL DEFAULT false, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "REL_8abe81b9aac151ae60bf507ad1" UNIQUE ("billing_address_id"), CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_fdb2f3ad8115da4c7718109a6e" ON "customer" ("email") `); + await queryRunner.query(`CREATE TABLE "address" ("id" character varying NOT NULL, "customer_id" character varying, "company" character varying, "first_name" character varying, "last_name" character varying, "address_1" character varying, "address_2" character varying, "city" character varying, "country_code" character varying, "province" character varying, "postal_code" character varying, "phone" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_d92de1f82754668b5f5f5dd4fd5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "idempotency_key" ("id" character varying NOT NULL, "idempotency_key" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "locked_at" TIMESTAMP WITH TIME ZONE, "request_method" character varying, "request_params" jsonb, "request_path" character varying, "response_code" integer, "response_body" jsonb, "recovery_point" character varying NOT NULL DEFAULT 'started', CONSTRAINT "PK_213f125e14469be304f9ff1d452" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a421bf4588d0004a9b0c0fe84f" ON "idempotency_key" ("idempotency_key") `); + await queryRunner.query(`CREATE TABLE "oauth" ("id" character varying NOT NULL, "display_name" character varying NOT NULL, "application_name" character varying NOT NULL, "install_url" character varying, "uninstall_url" character varying, "data" jsonb, CONSTRAINT "PK_a957b894e50eb16b969c0640a8d" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c49c061b1a686843c5d673506f" ON "oauth" ("application_name") `); + await queryRunner.query(`CREATE TABLE "staged_job" ("id" character varying NOT NULL, "event_name" character varying NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "PK_9a28fb48c46c5509faf43ac8c8d" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "store" ("id" character varying NOT NULL, "name" character varying NOT NULL DEFAULT 'Medusa Store', "default_currency_code" character varying NOT NULL DEFAULT 'usd', "swap_link_template" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_f3172007d4de5ae8e7692759d79" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "user" ("id" character varying NOT NULL, "email" character varying NOT NULL, "first_name" character varying, "last_name" character varying, "password_hash" character varying NOT NULL, "api_token" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e12875dfb3b1d92d7d7c5377e2" ON "user" ("email") `); + await queryRunner.query(`CREATE TABLE "region_payment_providers" ("region_id" character varying NOT NULL, "provider_id" character varying NOT NULL, CONSTRAINT "PK_9fa1e69914d3dd752de6b1da407" PRIMARY KEY ("region_id", "provider_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_8aaa78ba90d3802edac317df86" ON "region_payment_providers" ("region_id") `); + await queryRunner.query(`CREATE INDEX "IDX_3a6947180aeec283cd92c59ebb" ON "region_payment_providers" ("provider_id") `); + await queryRunner.query(`CREATE TABLE "region_fulfillment_providers" ("region_id" character varying NOT NULL, "provider_id" character varying NOT NULL, CONSTRAINT "PK_5b7d928a1fb50d6803868cfab3a" PRIMARY KEY ("region_id", "provider_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_c556e14eff4d6f03db593df955" ON "region_fulfillment_providers" ("region_id") `); + await queryRunner.query(`CREATE INDEX "IDX_37f361c38a18d12a3fa3158d0c" ON "region_fulfillment_providers" ("provider_id") `); + await queryRunner.query(`CREATE TABLE "product_images" ("product_id" character varying NOT NULL, "image_id" character varying NOT NULL, CONSTRAINT "PK_10de97980da2e939c4c0e8423f2" PRIMARY KEY ("product_id", "image_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_4f166bb8c2bfcef2498d97b406" ON "product_images" ("product_id") `); + await queryRunner.query(`CREATE INDEX "IDX_2212515ba306c79f42c46a99db" ON "product_images" ("image_id") `); + await queryRunner.query(`CREATE TABLE "discount_rule_products" ("discount_rule_id" character varying NOT NULL, "product_id" character varying NOT NULL, CONSTRAINT "PK_351c8c92f5d27283c445cd022ee" PRIMARY KEY ("discount_rule_id", "product_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_4e0739e5f0244c08d41174ca08" ON "discount_rule_products" ("discount_rule_id") `); + await queryRunner.query(`CREATE INDEX "IDX_be66106a673b88a81c603abe7e" ON "discount_rule_products" ("product_id") `); + await queryRunner.query(`CREATE TABLE "discount_regions" ("discount_id" character varying NOT NULL, "region_id" character varying NOT NULL, CONSTRAINT "PK_15974566a8b6e04a7c754e85b75" PRIMARY KEY ("discount_id", "region_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_f4194aa81073f3fab8aa86906f" ON "discount_regions" ("discount_id") `); + await queryRunner.query(`CREATE INDEX "IDX_a21a7ffbe420d492eb46c305fe" ON "discount_regions" ("region_id") `); + await queryRunner.query(`CREATE TABLE "cart_discounts" ("cart_id" character varying NOT NULL, "discount_id" character varying NOT NULL, CONSTRAINT "PK_10bd412c9071ccc0cf555afd9bb" PRIMARY KEY ("cart_id", "discount_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_6680319ebe1f46d18f106191d5" ON "cart_discounts" ("cart_id") `); + await queryRunner.query(`CREATE INDEX "IDX_8df75ef4f35f217768dc113545" ON "cart_discounts" ("discount_id") `); + await queryRunner.query(`CREATE TABLE "cart_gift_cards" ("cart_id" character varying NOT NULL, "gift_card_id" character varying NOT NULL, CONSTRAINT "PK_2389be82bf0ef3635e2014c9ef1" PRIMARY KEY ("cart_id", "gift_card_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_d38047a90f3d42f0be7909e8ae" ON "cart_gift_cards" ("cart_id") `); + await queryRunner.query(`CREATE INDEX "IDX_0fb38b6d167793192bc126d835" ON "cart_gift_cards" ("gift_card_id") `); + await queryRunner.query(`CREATE TABLE "order_discounts" ("order_id" character varying NOT NULL, "discount_id" character varying NOT NULL, CONSTRAINT "PK_a7418714ffceebc125bf6d8fcfe" PRIMARY KEY ("order_id", "discount_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_e7b488cebe333f449398769b2c" ON "order_discounts" ("order_id") `); + await queryRunner.query(`CREATE INDEX "IDX_0fc1ec4e3db9001ad60c19daf1" ON "order_discounts" ("discount_id") `); + await queryRunner.query(`CREATE TABLE "order_gift_cards" ("order_id" character varying NOT NULL, "gift_card_id" character varying NOT NULL, CONSTRAINT "PK_49a8ec66a6625d7c2e3526e05b4" PRIMARY KEY ("order_id", "gift_card_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_e62ff11e4730bb3adfead979ee" ON "order_gift_cards" ("order_id") `); + await queryRunner.query(`CREATE INDEX "IDX_f2bb9f71e95b315eb24b2b84cb" ON "order_gift_cards" ("gift_card_id") `); + await queryRunner.query(`CREATE TABLE "store_currencies" ("store_id" character varying NOT NULL, "currency_code" character varying NOT NULL, CONSTRAINT "PK_0f2bff3bccc785c320a4df836de" PRIMARY KEY ("store_id", "currency_code"))`); + await queryRunner.query(`CREATE INDEX "IDX_b4f4b63d1736689b7008980394" ON "store_currencies" ("store_id") `); + await queryRunner.query(`CREATE INDEX "IDX_82a6bbb0b527c20a0002ddcbd6" ON "store_currencies" ("currency_code") `); + await queryRunner.query(`ALTER TABLE "fulfillment_item" ADD CONSTRAINT "FK_a033f83cc6bd7701a5687ab4b38" FOREIGN KEY ("fulfillment_id") REFERENCES "fulfillment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "fulfillment_item" ADD CONSTRAINT "FK_e13ff60e74206b747a1896212d1" FOREIGN KEY ("item_id") REFERENCES "line_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "fulfillment" ADD CONSTRAINT "FK_a52e234f729db789cf473297a5c" FOREIGN KEY ("swap_id") REFERENCES "swap"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "fulfillment" ADD CONSTRAINT "FK_f129acc85e346a10eed12b86fca" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "fulfillment" ADD CONSTRAINT "FK_beb35a6de60a6c4f91d5ae57e44" FOREIGN KEY ("provider_id") REFERENCES "fulfillment_provider"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "return_item" ADD CONSTRAINT "FK_7edab75b4fc88ea6d4f2574f087" FOREIGN KEY ("return_id") REFERENCES "return"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "return_item" ADD CONSTRAINT "FK_87774591f44564effd8039d7162" FOREIGN KEY ("item_id") REFERENCES "line_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "country" ADD CONSTRAINT "FK_b1aac8314662fa6b25569a575bb" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "region" ADD CONSTRAINT "FK_3bdd5896ec93be2f1c62a3309a5" FOREIGN KEY ("currency_code") REFERENCES "currency"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "money_amount" ADD CONSTRAINT "FK_e15811f81339e4bd8c440aebe1c" FOREIGN KEY ("currency_code") REFERENCES "currency"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "money_amount" ADD CONSTRAINT "FK_17a06d728e4cfbc5bd2ddb70af0" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "money_amount" ADD CONSTRAINT "FK_b433e27b7a83e6d12ab26b15b03" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product_variant" ADD CONSTRAINT "FK_ca67dd080aac5ecf99609960cd2" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product_option_value" ADD CONSTRAINT "FK_cdf4388f294b30a25c627d69fe9" FOREIGN KEY ("option_id") REFERENCES "product_option"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product_option_value" ADD CONSTRAINT "FK_7234ed737ff4eb1b6ae6e6d7b01" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product_option" ADD CONSTRAINT "FK_e634fca34f6b594b87fdbee95f6" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_option_requirement" ADD CONSTRAINT "FK_012a62ba743e427b5ebe9dee18e" FOREIGN KEY ("shipping_option_id") REFERENCES "shipping_option"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_option" ADD CONSTRAINT "FK_5c58105f1752fca0f4ce69f4663" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_option" ADD CONSTRAINT "FK_c951439af4c98bf2bd7fb8726cd" FOREIGN KEY ("profile_id") REFERENCES "shipping_profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_option" ADD CONSTRAINT "FK_a0e206bfaed3cb63c1860917347" FOREIGN KEY ("provider_id") REFERENCES "fulfillment_provider"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product" ADD CONSTRAINT "FK_80823b7ae866dc5acae2dac6d2c" FOREIGN KEY ("profile_id") REFERENCES "shipping_profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "discount" ADD CONSTRAINT "FK_ac2c280de3701b2d66f6817f760" FOREIGN KEY ("rule_id") REFERENCES "discount_rule"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "discount" ADD CONSTRAINT "FK_2250c5d9e975987ab212f61a663" FOREIGN KEY ("parent_discount_id") REFERENCES "discount"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payment_session" ADD CONSTRAINT "FK_d25ba0787e1510ddc5d442ebcfa" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payment" ADD CONSTRAINT "FK_c17aff091441b7c25ec3d68d36c" FOREIGN KEY ("swap_id") REFERENCES "swap"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payment" ADD CONSTRAINT "FK_4665f17abc1e81dd58330e58542" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payment" ADD CONSTRAINT "FK_f5221735ace059250daac9d9803" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payment" ADD CONSTRAINT "FK_f41553459a4b1491c9893ebc921" FOREIGN KEY ("currency_code") REFERENCES "currency"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gift_card" ADD CONSTRAINT "FK_b6bcf8c3903097b84e85154eed3" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gift_card" ADD CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart" ADD CONSTRAINT "FK_6b9c66b5e36f7c827dfaa092f94" FOREIGN KEY ("billing_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart" ADD CONSTRAINT "FK_ced15a9a695d2b5db9dabce763d" FOREIGN KEY ("shipping_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart" ADD CONSTRAINT "FK_484c329f4783be4e18e5e2ff090" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart" ADD CONSTRAINT "FK_242205c81c1152fab1b6e848470" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart" ADD CONSTRAINT "FK_9d1a161434c610aae7c3df2dc7e" FOREIGN KEY ("payment_id") REFERENCES "payment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_5267705a43d547e232535b656c2" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_d92993a7d554d84571f4eea1d13" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_fb94fa8d5ca940daa2a58139f86" FOREIGN KEY ("swap_id") REFERENCES "swap"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_1d9ad62038998c3a85c77a53cfb" FOREIGN KEY ("return_id") REFERENCES "return"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_fc963e94854bff2714ca84cd193" FOREIGN KEY ("shipping_option_id") REFERENCES "shipping_option"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "return" ADD CONSTRAINT "FK_bad82d7bff2b08b87094bfac3d6" FOREIGN KEY ("swap_id") REFERENCES "swap"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "return" ADD CONSTRAINT "FK_d4bd17f918fc6c332b74a368c36" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "swap" ADD CONSTRAINT "FK_52dd74e8c989aa5665ad2852b8b" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "swap" ADD CONSTRAINT "FK_f5189d38b3d3bd496618bf54c57" FOREIGN KEY ("shipping_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "swap" ADD CONSTRAINT "FK_402e8182bc553e082f6380020b4" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "line_item" ADD CONSTRAINT "FK_27283ee631862266d0f1c680646" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "line_item" ADD CONSTRAINT "FK_43a2b24495fe1d9fc2a9c835bc7" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "line_item" ADD CONSTRAINT "FK_3fa354d8d1233ff81097b2fcb6b" FOREIGN KEY ("swap_id") REFERENCES "swap"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "line_item" ADD CONSTRAINT "FK_5371cbaa3be5200f373d24e3d5b" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gift_card_transaction" ADD CONSTRAINT "FK_3ff5597f1d7e02bba41541846f4" FOREIGN KEY ("gift_card_id") REFERENCES "gift_card"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gift_card_transaction" ADD CONSTRAINT "FK_d7d441b81012f87d4265fa57d24" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "refund" ADD CONSTRAINT "FK_eec9d9af4ca098e19ea6b499eaa" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_c99a206eb11ad45f6b7f04f2dcc" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_cd7812c96209c5bdd48a6b858b0" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_5568d3b9ce9f7abeeb37511ecf2" FOREIGN KEY ("billing_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_19b0c6293443d1b464f604c3316" FOREIGN KEY ("shipping_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_e1fcce2b18dbcdbe0a5ba9a68b8" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_717a141f96b76d794d409f38129" FOREIGN KEY ("currency_code") REFERENCES "currency"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "customer" ADD CONSTRAINT "FK_8abe81b9aac151ae60bf507ad15" FOREIGN KEY ("billing_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "address" ADD CONSTRAINT "FK_9c9614b2f9d01665800ea8dbff7" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "address" ADD CONSTRAINT "FK_6df8c6bf969a51d24c1980c4ff4" FOREIGN KEY ("country_code") REFERENCES "country"("iso_2") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "store" ADD CONSTRAINT "FK_55beebaa09e947cccca554af222" FOREIGN KEY ("default_currency_code") REFERENCES "currency"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "region_payment_providers" ADD CONSTRAINT "FK_8aaa78ba90d3802edac317df869" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "region_payment_providers" ADD CONSTRAINT "FK_3a6947180aeec283cd92c59ebb0" FOREIGN KEY ("provider_id") REFERENCES "payment_provider"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "region_fulfillment_providers" ADD CONSTRAINT "FK_c556e14eff4d6f03db593df955e" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "region_fulfillment_providers" ADD CONSTRAINT "FK_37f361c38a18d12a3fa3158d0cf" FOREIGN KEY ("provider_id") REFERENCES "fulfillment_provider"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product_images" ADD CONSTRAINT "FK_4f166bb8c2bfcef2498d97b4068" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "product_images" ADD CONSTRAINT "FK_2212515ba306c79f42c46a99db7" FOREIGN KEY ("image_id") REFERENCES "image"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "discount_rule_products" ADD CONSTRAINT "FK_4e0739e5f0244c08d41174ca08a" FOREIGN KEY ("discount_rule_id") REFERENCES "discount_rule"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "discount_rule_products" ADD CONSTRAINT "FK_be66106a673b88a81c603abe7eb" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "discount_regions" ADD CONSTRAINT "FK_f4194aa81073f3fab8aa86906ff" FOREIGN KEY ("discount_id") REFERENCES "discount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "discount_regions" ADD CONSTRAINT "FK_a21a7ffbe420d492eb46c305fec" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart_discounts" ADD CONSTRAINT "FK_6680319ebe1f46d18f106191d59" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart_discounts" ADD CONSTRAINT "FK_8df75ef4f35f217768dc1135458" FOREIGN KEY ("discount_id") REFERENCES "discount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart_gift_cards" ADD CONSTRAINT "FK_d38047a90f3d42f0be7909e8aea" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "cart_gift_cards" ADD CONSTRAINT "FK_0fb38b6d167793192bc126d835e" FOREIGN KEY ("gift_card_id") REFERENCES "gift_card"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order_discounts" ADD CONSTRAINT "FK_e7b488cebe333f449398769b2cc" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order_discounts" ADD CONSTRAINT "FK_0fc1ec4e3db9001ad60c19daf16" FOREIGN KEY ("discount_id") REFERENCES "discount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order_gift_cards" ADD CONSTRAINT "FK_e62ff11e4730bb3adfead979ee2" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order_gift_cards" ADD CONSTRAINT "FK_f2bb9f71e95b315eb24b2b84cb3" FOREIGN KEY ("gift_card_id") REFERENCES "gift_card"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "store_currencies" ADD CONSTRAINT "FK_b4f4b63d1736689b7008980394c" FOREIGN KEY ("store_id") REFERENCES "store"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "store_currencies" ADD CONSTRAINT "FK_82a6bbb0b527c20a0002ddcbd60" FOREIGN KEY ("currency_code") REFERENCES "currency"("code") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "store_currencies" DROP CONSTRAINT "FK_82a6bbb0b527c20a0002ddcbd60"`); + await queryRunner.query(`ALTER TABLE "store_currencies" DROP CONSTRAINT "FK_b4f4b63d1736689b7008980394c"`); + await queryRunner.query(`ALTER TABLE "order_gift_cards" DROP CONSTRAINT "FK_f2bb9f71e95b315eb24b2b84cb3"`); + await queryRunner.query(`ALTER TABLE "order_gift_cards" DROP CONSTRAINT "FK_e62ff11e4730bb3adfead979ee2"`); + await queryRunner.query(`ALTER TABLE "order_discounts" DROP CONSTRAINT "FK_0fc1ec4e3db9001ad60c19daf16"`); + await queryRunner.query(`ALTER TABLE "order_discounts" DROP CONSTRAINT "FK_e7b488cebe333f449398769b2cc"`); + await queryRunner.query(`ALTER TABLE "cart_gift_cards" DROP CONSTRAINT "FK_0fb38b6d167793192bc126d835e"`); + await queryRunner.query(`ALTER TABLE "cart_gift_cards" DROP CONSTRAINT "FK_d38047a90f3d42f0be7909e8aea"`); + await queryRunner.query(`ALTER TABLE "cart_discounts" DROP CONSTRAINT "FK_8df75ef4f35f217768dc1135458"`); + await queryRunner.query(`ALTER TABLE "cart_discounts" DROP CONSTRAINT "FK_6680319ebe1f46d18f106191d59"`); + await queryRunner.query(`ALTER TABLE "discount_regions" DROP CONSTRAINT "FK_a21a7ffbe420d492eb46c305fec"`); + await queryRunner.query(`ALTER TABLE "discount_regions" DROP CONSTRAINT "FK_f4194aa81073f3fab8aa86906ff"`); + await queryRunner.query(`ALTER TABLE "discount_rule_products" DROP CONSTRAINT "FK_be66106a673b88a81c603abe7eb"`); + await queryRunner.query(`ALTER TABLE "discount_rule_products" DROP CONSTRAINT "FK_4e0739e5f0244c08d41174ca08a"`); + await queryRunner.query(`ALTER TABLE "product_images" DROP CONSTRAINT "FK_2212515ba306c79f42c46a99db7"`); + await queryRunner.query(`ALTER TABLE "product_images" DROP CONSTRAINT "FK_4f166bb8c2bfcef2498d97b4068"`); + await queryRunner.query(`ALTER TABLE "region_fulfillment_providers" DROP CONSTRAINT "FK_37f361c38a18d12a3fa3158d0cf"`); + await queryRunner.query(`ALTER TABLE "region_fulfillment_providers" DROP CONSTRAINT "FK_c556e14eff4d6f03db593df955e"`); + await queryRunner.query(`ALTER TABLE "region_payment_providers" DROP CONSTRAINT "FK_3a6947180aeec283cd92c59ebb0"`); + await queryRunner.query(`ALTER TABLE "region_payment_providers" DROP CONSTRAINT "FK_8aaa78ba90d3802edac317df869"`); + await queryRunner.query(`ALTER TABLE "store" DROP CONSTRAINT "FK_55beebaa09e947cccca554af222"`); + await queryRunner.query(`ALTER TABLE "address" DROP CONSTRAINT "FK_6df8c6bf969a51d24c1980c4ff4"`); + await queryRunner.query(`ALTER TABLE "address" DROP CONSTRAINT "FK_9c9614b2f9d01665800ea8dbff7"`); + await queryRunner.query(`ALTER TABLE "customer" DROP CONSTRAINT "FK_8abe81b9aac151ae60bf507ad15"`); + await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_717a141f96b76d794d409f38129"`); + await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_e1fcce2b18dbcdbe0a5ba9a68b8"`); + await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_19b0c6293443d1b464f604c3316"`); + await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_5568d3b9ce9f7abeeb37511ecf2"`); + await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_cd7812c96209c5bdd48a6b858b0"`); + await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_c99a206eb11ad45f6b7f04f2dcc"`); + await queryRunner.query(`ALTER TABLE "refund" DROP CONSTRAINT "FK_eec9d9af4ca098e19ea6b499eaa"`); + await queryRunner.query(`ALTER TABLE "gift_card_transaction" DROP CONSTRAINT "FK_d7d441b81012f87d4265fa57d24"`); + await queryRunner.query(`ALTER TABLE "gift_card_transaction" DROP CONSTRAINT "FK_3ff5597f1d7e02bba41541846f4"`); + await queryRunner.query(`ALTER TABLE "line_item" DROP CONSTRAINT "FK_5371cbaa3be5200f373d24e3d5b"`); + await queryRunner.query(`ALTER TABLE "line_item" DROP CONSTRAINT "FK_3fa354d8d1233ff81097b2fcb6b"`); + await queryRunner.query(`ALTER TABLE "line_item" DROP CONSTRAINT "FK_43a2b24495fe1d9fc2a9c835bc7"`); + await queryRunner.query(`ALTER TABLE "line_item" DROP CONSTRAINT "FK_27283ee631862266d0f1c680646"`); + await queryRunner.query(`ALTER TABLE "swap" DROP CONSTRAINT "FK_402e8182bc553e082f6380020b4"`); + await queryRunner.query(`ALTER TABLE "swap" DROP CONSTRAINT "FK_f5189d38b3d3bd496618bf54c57"`); + await queryRunner.query(`ALTER TABLE "swap" DROP CONSTRAINT "FK_52dd74e8c989aa5665ad2852b8b"`); + await queryRunner.query(`ALTER TABLE "return" DROP CONSTRAINT "FK_d4bd17f918fc6c332b74a368c36"`); + await queryRunner.query(`ALTER TABLE "return" DROP CONSTRAINT "FK_bad82d7bff2b08b87094bfac3d6"`); + await queryRunner.query(`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_fc963e94854bff2714ca84cd193"`); + await queryRunner.query(`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_1d9ad62038998c3a85c77a53cfb"`); + await queryRunner.query(`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_fb94fa8d5ca940daa2a58139f86"`); + await queryRunner.query(`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_d92993a7d554d84571f4eea1d13"`); + await queryRunner.query(`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_5267705a43d547e232535b656c2"`); + await queryRunner.query(`ALTER TABLE "cart" DROP CONSTRAINT "FK_9d1a161434c610aae7c3df2dc7e"`); + await queryRunner.query(`ALTER TABLE "cart" DROP CONSTRAINT "FK_242205c81c1152fab1b6e848470"`); + await queryRunner.query(`ALTER TABLE "cart" DROP CONSTRAINT "FK_484c329f4783be4e18e5e2ff090"`); + await queryRunner.query(`ALTER TABLE "cart" DROP CONSTRAINT "FK_ced15a9a695d2b5db9dabce763d"`); + await queryRunner.query(`ALTER TABLE "cart" DROP CONSTRAINT "FK_6b9c66b5e36f7c827dfaa092f94"`); + await queryRunner.query(`ALTER TABLE "gift_card" DROP CONSTRAINT "FK_dfc1f02bb0552e79076aa58dbb0"`); + await queryRunner.query(`ALTER TABLE "gift_card" DROP CONSTRAINT "FK_b6bcf8c3903097b84e85154eed3"`); + await queryRunner.query(`ALTER TABLE "payment" DROP CONSTRAINT "FK_f41553459a4b1491c9893ebc921"`); + await queryRunner.query(`ALTER TABLE "payment" DROP CONSTRAINT "FK_f5221735ace059250daac9d9803"`); + await queryRunner.query(`ALTER TABLE "payment" DROP CONSTRAINT "FK_4665f17abc1e81dd58330e58542"`); + await queryRunner.query(`ALTER TABLE "payment" DROP CONSTRAINT "FK_c17aff091441b7c25ec3d68d36c"`); + await queryRunner.query(`ALTER TABLE "payment_session" DROP CONSTRAINT "FK_d25ba0787e1510ddc5d442ebcfa"`); + await queryRunner.query(`ALTER TABLE "discount" DROP CONSTRAINT "FK_2250c5d9e975987ab212f61a663"`); + await queryRunner.query(`ALTER TABLE "discount" DROP CONSTRAINT "FK_ac2c280de3701b2d66f6817f760"`); + await queryRunner.query(`ALTER TABLE "product" DROP CONSTRAINT "FK_80823b7ae866dc5acae2dac6d2c"`); + await queryRunner.query(`ALTER TABLE "shipping_option" DROP CONSTRAINT "FK_a0e206bfaed3cb63c1860917347"`); + await queryRunner.query(`ALTER TABLE "shipping_option" DROP CONSTRAINT "FK_c951439af4c98bf2bd7fb8726cd"`); + await queryRunner.query(`ALTER TABLE "shipping_option" DROP CONSTRAINT "FK_5c58105f1752fca0f4ce69f4663"`); + await queryRunner.query(`ALTER TABLE "shipping_option_requirement" DROP CONSTRAINT "FK_012a62ba743e427b5ebe9dee18e"`); + await queryRunner.query(`ALTER TABLE "product_option" DROP CONSTRAINT "FK_e634fca34f6b594b87fdbee95f6"`); + await queryRunner.query(`ALTER TABLE "product_option_value" DROP CONSTRAINT "FK_7234ed737ff4eb1b6ae6e6d7b01"`); + await queryRunner.query(`ALTER TABLE "product_option_value" DROP CONSTRAINT "FK_cdf4388f294b30a25c627d69fe9"`); + await queryRunner.query(`ALTER TABLE "product_variant" DROP CONSTRAINT "FK_ca67dd080aac5ecf99609960cd2"`); + await queryRunner.query(`ALTER TABLE "money_amount" DROP CONSTRAINT "FK_b433e27b7a83e6d12ab26b15b03"`); + await queryRunner.query(`ALTER TABLE "money_amount" DROP CONSTRAINT "FK_17a06d728e4cfbc5bd2ddb70af0"`); + await queryRunner.query(`ALTER TABLE "money_amount" DROP CONSTRAINT "FK_e15811f81339e4bd8c440aebe1c"`); + await queryRunner.query(`ALTER TABLE "region" DROP CONSTRAINT "FK_3bdd5896ec93be2f1c62a3309a5"`); + await queryRunner.query(`ALTER TABLE "country" DROP CONSTRAINT "FK_b1aac8314662fa6b25569a575bb"`); + await queryRunner.query(`ALTER TABLE "return_item" DROP CONSTRAINT "FK_87774591f44564effd8039d7162"`); + await queryRunner.query(`ALTER TABLE "return_item" DROP CONSTRAINT "FK_7edab75b4fc88ea6d4f2574f087"`); + await queryRunner.query(`ALTER TABLE "fulfillment" DROP CONSTRAINT "FK_beb35a6de60a6c4f91d5ae57e44"`); + await queryRunner.query(`ALTER TABLE "fulfillment" DROP CONSTRAINT "FK_f129acc85e346a10eed12b86fca"`); + await queryRunner.query(`ALTER TABLE "fulfillment" DROP CONSTRAINT "FK_a52e234f729db789cf473297a5c"`); + await queryRunner.query(`ALTER TABLE "fulfillment_item" DROP CONSTRAINT "FK_e13ff60e74206b747a1896212d1"`); + await queryRunner.query(`ALTER TABLE "fulfillment_item" DROP CONSTRAINT "FK_a033f83cc6bd7701a5687ab4b38"`); + await queryRunner.query(`DROP INDEX "IDX_82a6bbb0b527c20a0002ddcbd6"`); + await queryRunner.query(`DROP INDEX "IDX_b4f4b63d1736689b7008980394"`); + await queryRunner.query(`DROP TABLE "store_currencies"`); + await queryRunner.query(`DROP INDEX "IDX_f2bb9f71e95b315eb24b2b84cb"`); + await queryRunner.query(`DROP INDEX "IDX_e62ff11e4730bb3adfead979ee"`); + await queryRunner.query(`DROP TABLE "order_gift_cards"`); + await queryRunner.query(`DROP INDEX "IDX_0fc1ec4e3db9001ad60c19daf1"`); + await queryRunner.query(`DROP INDEX "IDX_e7b488cebe333f449398769b2c"`); + await queryRunner.query(`DROP TABLE "order_discounts"`); + await queryRunner.query(`DROP INDEX "IDX_0fb38b6d167793192bc126d835"`); + await queryRunner.query(`DROP INDEX "IDX_d38047a90f3d42f0be7909e8ae"`); + await queryRunner.query(`DROP TABLE "cart_gift_cards"`); + await queryRunner.query(`DROP INDEX "IDX_8df75ef4f35f217768dc113545"`); + await queryRunner.query(`DROP INDEX "IDX_6680319ebe1f46d18f106191d5"`); + await queryRunner.query(`DROP TABLE "cart_discounts"`); + await queryRunner.query(`DROP INDEX "IDX_a21a7ffbe420d492eb46c305fe"`); + await queryRunner.query(`DROP INDEX "IDX_f4194aa81073f3fab8aa86906f"`); + await queryRunner.query(`DROP TABLE "discount_regions"`); + await queryRunner.query(`DROP INDEX "IDX_be66106a673b88a81c603abe7e"`); + await queryRunner.query(`DROP INDEX "IDX_4e0739e5f0244c08d41174ca08"`); + await queryRunner.query(`DROP TABLE "discount_rule_products"`); + await queryRunner.query(`DROP INDEX "IDX_2212515ba306c79f42c46a99db"`); + await queryRunner.query(`DROP INDEX "IDX_4f166bb8c2bfcef2498d97b406"`); + await queryRunner.query(`DROP TABLE "product_images"`); + await queryRunner.query(`DROP INDEX "IDX_37f361c38a18d12a3fa3158d0c"`); + await queryRunner.query(`DROP INDEX "IDX_c556e14eff4d6f03db593df955"`); + await queryRunner.query(`DROP TABLE "region_fulfillment_providers"`); + await queryRunner.query(`DROP INDEX "IDX_3a6947180aeec283cd92c59ebb"`); + await queryRunner.query(`DROP INDEX "IDX_8aaa78ba90d3802edac317df86"`); + await queryRunner.query(`DROP TABLE "region_payment_providers"`); + await queryRunner.query(`DROP INDEX "IDX_e12875dfb3b1d92d7d7c5377e2"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP TABLE "store"`); + await queryRunner.query(`DROP TABLE "staged_job"`); + await queryRunner.query(`DROP INDEX "IDX_c49c061b1a686843c5d673506f"`); + await queryRunner.query(`DROP TABLE "oauth"`); + await queryRunner.query(`DROP INDEX "IDX_a421bf4588d0004a9b0c0fe84f"`); + await queryRunner.query(`DROP TABLE "idempotency_key"`); + await queryRunner.query(`DROP TABLE "address"`); + await queryRunner.query(`DROP INDEX "IDX_fdb2f3ad8115da4c7718109a6e"`); + await queryRunner.query(`DROP TABLE "customer"`); + await queryRunner.query(`DROP TABLE "order"`); + await queryRunner.query(`DROP TYPE "order_payment_status_enum"`); + await queryRunner.query(`DROP TYPE "order_fulfillment_status_enum"`); + await queryRunner.query(`DROP TYPE "order_status_enum"`); + await queryRunner.query(`DROP TABLE "refund"`); + await queryRunner.query(`DROP TYPE "refund_reason_enum"`); + await queryRunner.query(`DROP TABLE "gift_card_transaction"`); + await queryRunner.query(`DROP INDEX "IDX_5371cbaa3be5200f373d24e3d5"`); + await queryRunner.query(`DROP INDEX "IDX_3fa354d8d1233ff81097b2fcb6"`); + await queryRunner.query(`DROP INDEX "IDX_43a2b24495fe1d9fc2a9c835bc"`); + await queryRunner.query(`DROP INDEX "IDX_27283ee631862266d0f1c68064"`); + await queryRunner.query(`DROP TABLE "line_item"`); + await queryRunner.query(`DROP TABLE "swap"`); + await queryRunner.query(`DROP TYPE "swap_payment_status_enum"`); + await queryRunner.query(`DROP TYPE "swap_fulfillment_status_enum"`); + await queryRunner.query(`DROP TABLE "return"`); + await queryRunner.query(`DROP TYPE "return_status_enum"`); + await queryRunner.query(`DROP INDEX "IDX_1d9ad62038998c3a85c77a53cf"`); + await queryRunner.query(`DROP INDEX "IDX_fb94fa8d5ca940daa2a58139f8"`); + await queryRunner.query(`DROP INDEX "IDX_d92993a7d554d84571f4eea1d1"`); + await queryRunner.query(`DROP INDEX "IDX_5267705a43d547e232535b656c"`); + await queryRunner.query(`DROP INDEX "IDX_fc963e94854bff2714ca84cd19"`); + await queryRunner.query(`DROP TABLE "shipping_method"`); + await queryRunner.query(`DROP TABLE "cart"`); + await queryRunner.query(`DROP TYPE "cart_type_enum"`); + await queryRunner.query(`DROP INDEX "IDX_53cb5605fa42e82b4d47b47bda"`); + await queryRunner.query(`DROP TABLE "gift_card"`); + await queryRunner.query(`DROP TABLE "payment"`); + await queryRunner.query(`DROP TABLE "payment_session"`); + await queryRunner.query(`DROP TYPE "payment_session_status_enum"`); + await queryRunner.query(`DROP INDEX "IDX_087926f6fec32903be3c8eedfa"`); + await queryRunner.query(`DROP TABLE "discount"`); + await queryRunner.query(`DROP TABLE "discount_rule"`); + await queryRunner.query(`DROP TYPE "discount_rule_allocation_enum"`); + await queryRunner.query(`DROP TYPE "discount_rule_type_enum"`); + await queryRunner.query(`DROP INDEX "IDX_db7355f7bd36c547c8a4f539e5"`); + await queryRunner.query(`DROP TABLE "product"`); + await queryRunner.query(`DROP TABLE "shipping_profile"`); + await queryRunner.query(`DROP TYPE "shipping_profile_type_enum"`); + await queryRunner.query(`DROP TABLE "shipping_option"`); + await queryRunner.query(`DROP TYPE "shipping_option_price_type_enum"`); + await queryRunner.query(`DROP TABLE "shipping_option_requirement"`); + await queryRunner.query(`DROP TYPE "shipping_option_requirement_type_enum"`); + await queryRunner.query(`DROP TABLE "product_option"`); + await queryRunner.query(`DROP INDEX "IDX_cdf4388f294b30a25c627d69fe"`); + await queryRunner.query(`DROP TABLE "product_option_value"`); + await queryRunner.query(`DROP INDEX "IDX_a0a3f124dc5b167622217fee02"`); + await queryRunner.query(`DROP INDEX "IDX_7124082c8846a06a857cca386c"`); + await queryRunner.query(`DROP INDEX "IDX_9db95c4b71f632fc93ecbc3d8b"`); + await queryRunner.query(`DROP INDEX "IDX_f4dc2c0888b66d547c175f090e"`); + await queryRunner.query(`DROP TABLE "product_variant"`); + await queryRunner.query(`DROP TABLE "money_amount"`); + await queryRunner.query(`DROP TABLE "image"`); + await queryRunner.query(`DROP TABLE "region"`); + await queryRunner.query(`DROP TABLE "payment_provider"`); + await queryRunner.query(`DROP INDEX "IDX_e78901b1131eaf8203d9b1cb5f"`); + await queryRunner.query(`DROP TABLE "country"`); + await queryRunner.query(`DROP TABLE "currency"`); + await queryRunner.query(`DROP TABLE "return_item"`); + await queryRunner.query(`DROP TABLE "fulfillment"`); + await queryRunner.query(`DROP TABLE "fulfillment_item"`); + await queryRunner.query(`DROP TABLE "fulfillment_provider"`); + } + +} diff --git a/packages/medusa/src/migrations/1611063174563-countries_currencies.ts b/packages/medusa/src/migrations/1611063174563-countries_currencies.ts new file mode 100644 index 0000000000..1f4a05e8aa --- /dev/null +++ b/packages/medusa/src/migrations/1611063174563-countries_currencies.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { countries } from "../utils/countries" +import { currencies } from "../utils/currencies" + +export class countriesCurrencies1611063174563 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + for (const c of countries) { + const query = `INSERT INTO "country" ("iso_2", "iso_3", "num_code", "name", "display_name") VALUES ($1, $2, $3, $4, $5)` + + const iso2 = c.alpha2.toLowerCase() + const iso3 = c.alpha3.toLowerCase() + const numeric = c.numeric + const name = c.name.toUpperCase() + const display = c.name + + await queryRunner.query(query, [iso2, iso3, numeric, name, display]) + } + + for (const [_, c] of Object.entries(currencies)) { + const query = `INSERT INTO "currency" ("code", "symbol", "symbol_native", "name") VALUES ($1, $2, $3, $4)` + + const code = c.code.toLowerCase() + const sym = c.symbol + const nat = c.symbol_native + const name = c.name + + await queryRunner.query(query, [code, sym, nat, name]) + } + } + + public async down(queryRunner: QueryRunner): Promise { + for (const c of countries) { + await queryRunner.query( + `DELETE FROM "country" WHERE iso_2 = '${c.alpha2}'` + ) + } + + for (const [_, c] of Object.entries(currencies)) { + await queryRunner.query( + `DELETE FROM "currency" WHERE code = '${c.code.toLowerCase()}'` + ) + } + } +} diff --git a/packages/medusa/src/models/address.ts b/packages/medusa/src/models/address.ts new file mode 100644 index 0000000000..7ef495082b --- /dev/null +++ b/packages/medusa/src/models/address.ts @@ -0,0 +1,81 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + ManyToOne, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Customer } from "./customer" +import { Country } from "./country" + +@Entity() +export class Address { + @PrimaryColumn() + id: string + + @Column({ nullable: true }) + customer_id: string + + @ManyToOne(() => Customer) + @JoinColumn({ name: "customer_id" }) + customer: Customer + + @Column({ nullable: true }) + company: string + + @Column({ nullable: true }) + first_name: string + + @Column({ nullable: true }) + last_name: string + + @Column({ nullable: true }) + address_1: string + + @Column({ nullable: true }) + address_2: string + + @Column({ nullable: true }) + city: string + + @Column({ nullable: true }) + country_code: string + + @ManyToOne(() => Country) + @JoinColumn({ name: "country_code", referencedColumnName: "iso_2" }) + country: Country + + @Column({ nullable: true }) + province: string + + @Column({ nullable: true }) + postal_code: string + + @Column({ nullable: true }) + phone: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `addr_${id}` + } +} diff --git a/packages/medusa/src/models/cart.js b/packages/medusa/src/models/cart.js deleted file mode 100644 index ed52c250ed..0000000000 --- a/packages/medusa/src/models/cart.js +++ /dev/null @@ -1,38 +0,0 @@ -/******************************************************************************* - * - ******************************************************************************/ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import LineItemSchema from "./schemas/line-item" -import PaymentMethodSchema from "./schemas/payment-method" -import ShippingMethodSchema from "./schemas/shipping-method" -import AddressSchema from "./schemas/address" -import DiscountSchema from "./schemas/discount" - -class CartModel extends BaseModel { - static modelName = "Cart" - - static schemaOptions = { - minimize: false, - } - - static schema = { - email: { type: String }, - billing_address: { type: AddressSchema }, - shipping_address: { type: AddressSchema }, - items: { type: [LineItemSchema], default: [] }, - region_id: { type: String, required: true }, - discounts: { type: [DiscountSchema], default: [] }, - customer_id: { type: String, default: "" }, - payment_sessions: { type: [PaymentMethodSchema], default: [] }, - shipping_options: { type: [ShippingMethodSchema], default: [] }, - payment_method: { type: PaymentMethodSchema }, - shipping_methods: { type: [ShippingMethodSchema], default: [] }, - is_swap: { type: Boolean, default: false }, - created: { type: String, default: Date.now }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default CartModel diff --git a/packages/medusa/src/models/cart.ts b/packages/medusa/src/models/cart.ts new file mode 100644 index 0000000000..4c266341cb --- /dev/null +++ b/packages/medusa/src/models/cart.ts @@ -0,0 +1,179 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, + AfterLoad, + Timestamp, + BeforeUpdate, +} from "typeorm" +import { ulid } from "ulid" + +import { Region } from "./region" +import { Address } from "./address" +import { LineItem } from "./line-item" +import { Discount } from "./discount" +import { Customer } from "./customer" +import { PaymentSession } from "./payment-session" +import { Payment } from "./payment" +import { GiftCard } from "./gift-card" +import { ShippingMethod } from "./shipping-method" + +export enum CartType { + DEFAULT = "default", + SWAP = "swap", + PAYMENT_LINK = "payment_link", +} + +@Entity() +export class Cart { + @PrimaryColumn() + id: string + + @Column({ nullable: true }) + email: string + + @Column({ nullable: true }) + billing_address_id: string + + @ManyToOne(() => Address, { + cascade: ["insert", "remove", "soft-remove"], + }) + @JoinColumn({ name: "billing_address_id" }) + billing_address: Address + + @Column({ nullable: true }) + shipping_address_id: string + + @ManyToOne(() => Address, { + cascade: ["insert", "remove", "soft-remove"], + }) + @JoinColumn({ name: "shipping_address_id" }) + shipping_address: Address + + @OneToMany( + () => LineItem, + lineItem => lineItem.cart, + { cascade: ["insert", "remove"] } + ) + items: LineItem[] + + @Column() + region_id: string + + @ManyToOne(() => Region) + @JoinColumn({ name: "region_id" }) + region: Region + + @ManyToMany(() => Discount) + @JoinTable({ + name: "cart_discounts", + joinColumn: { + name: "cart_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "discount_id", + referencedColumnName: "id", + }, + }) + discounts: Discount + + @ManyToMany(() => GiftCard) + @JoinTable({ + name: "cart_gift_cards", + joinColumn: { + name: "cart_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "gift_card_id", + referencedColumnName: "id", + }, + }) + gift_cards: GiftCard + + @Column({ nullable: true }) + customer_id: string + + @ManyToOne(() => Customer) + @JoinColumn({ name: "customer_id" }) + customer: Customer + + payment_session: PaymentSession + + @OneToMany( + () => PaymentSession, + paymentSession => paymentSession.cart, + { cascade: true } + ) + payment_sessions: PaymentSession[] + + @Column({ nullable: true }) + payment_id: string + + @OneToOne(() => Payment) + @JoinColumn({ name: "payment_id" }) + payment: Payment + + @OneToMany( + () => ShippingMethod, + method => method.cart, + { cascade: ["soft-remove", "remove"] } + ) + shipping_methods: ShippingMethod[] + + @Column({ type: "enum", enum: CartType, default: "default" }) + type: boolean + + @Column({ type: "timestamptz", nullable: true }) + completed_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + // Total fields + shipping_total: number + discount_total: number + tax_total: number + refunded_total: number + total: number + subtotal: number + refundable_amount: number + gift_card_total: number + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `cart_${id}` + } + + @AfterLoad() + private afterLoad() { + if (this.payment_sessions) { + this.payment_session = this.payment_sessions.find(p => p.is_selected) + } + } +} diff --git a/packages/medusa/src/models/counter.js b/packages/medusa/src/models/counter.js deleted file mode 100644 index 16b9a9ea7a..0000000000 --- a/packages/medusa/src/models/counter.js +++ /dev/null @@ -1,13 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class CounterModel extends BaseModel { - static modelName = "Counter" - - static schema = { - _id: String, - next: Number, - } -} - -export default CounterModel diff --git a/packages/medusa/src/models/country.ts b/packages/medusa/src/models/country.ts new file mode 100644 index 0000000000..ed3d366c48 --- /dev/null +++ b/packages/medusa/src/models/country.ts @@ -0,0 +1,42 @@ +import { + Entity, + Column, + ManyToOne, + JoinColumn, + Index, + PrimaryGeneratedColumn, +} from "typeorm" + +import { Region } from "./region" + +@Entity() +export class Country { + @PrimaryGeneratedColumn() + id: number + + @Index({ unique: true }) + @Column() + iso_2: string + + @Column() + iso_3: string + + @Column() + num_code: number + + @Column() + name: string + + @Column() + display_name: string + + @Column({ nullable: true }) + region_id: string + + @ManyToOne( + () => Region, + r => r.countries + ) + @JoinColumn({ name: "region_id" }) + region: Region +} diff --git a/packages/medusa/src/models/currency.ts b/packages/medusa/src/models/currency.ts new file mode 100644 index 0000000000..130c50608b --- /dev/null +++ b/packages/medusa/src/models/currency.ts @@ -0,0 +1,16 @@ +import { Entity, Column, PrimaryColumn } from "typeorm" + +@Entity() +export class Currency { + @PrimaryColumn() + code: string + + @Column() + symbol: string + + @Column() + symbol_native: string + + @Column() + name: string +} diff --git a/packages/medusa/src/models/customer.js b/packages/medusa/src/models/customer.js deleted file mode 100644 index 420ebceab3..0000000000 --- a/packages/medusa/src/models/customer.js +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * - ******************************************************************************/ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import AddressSchema from "./schemas/address" - -class CustomerModel extends BaseModel { - static modelName = "Customer" - - static schema = { - email: { type: String, required: true, unique: true }, - first_name: { type: String }, - last_name: { type: String }, - billing_address: { type: AddressSchema }, - payment_methods: { type: [mongoose.Schema.Types.Mixed], default: [] }, - shipping_addresses: { type: [AddressSchema], default: [] }, - password_hash: { type: String }, - phone: { type: String, default: "" }, - has_account: { type: Boolean, default: false }, - orders: { type: [String], default: [] }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default CustomerModel diff --git a/packages/medusa/src/models/customer.ts b/packages/medusa/src/models/customer.ts new file mode 100644 index 0000000000..1dc1c14e83 --- /dev/null +++ b/packages/medusa/src/models/customer.ts @@ -0,0 +1,80 @@ +import { + Entity, + BeforeInsert, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, + Column, + PrimaryColumn, + OneToOne, + OneToMany, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Address } from "./address" +import { Order } from "./order" + +@Entity() +export class Customer { + @PrimaryColumn() + id: string + + @Index({ unique: true }) + @Column() + email: string + + @Column({ nullable: true }) + first_name: string + + @Column({ nullable: true }) + last_name: string + + @Column({ nullable: true }) + billing_address_id: string + + @OneToOne(() => Address) + @JoinColumn({ name: "billing_address_id" }) + billing_address: Address + + @OneToMany( + () => Address, + address => address.customer + ) + shipping_addresses: Address[] + + @Column({ nullable: true }) + password_hash: string + + @Column({ nullable: true }) + phone: string + + @Column({ default: false }) + has_account: boolean + + @OneToMany( + () => Order, + order => order.customer + ) + orders: Order[] + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `cus_${id}` + } +} diff --git a/packages/medusa/src/models/discount-rule.ts b/packages/medusa/src/models/discount-rule.ts new file mode 100644 index 0000000000..aa2dc19732 --- /dev/null +++ b/packages/medusa/src/models/discount-rule.ts @@ -0,0 +1,87 @@ +import { + Entity, + BeforeInsert, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, + Column, + PrimaryColumn, + OneToOne, + ManyToMany, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Product } from "./product" + +export enum DiscountRuleType { + FIXED = "fixed", + PERCENTAGE = "percentage", + FREE_SHIPPING = "free_shipping", +} + +export enum AllocationType { + TOTAL = "total", + ITEM = "item", +} + +@Entity() +export class DiscountRule { + @PrimaryColumn() + id: string + + @Column() + description: string + + @Column({ + type: "enum", + enum: DiscountRuleType, + }) + type: DiscountRuleType + + @Column() + value: number + + @Column({ + type: "enum", + enum: AllocationType, + nullable: true, + }) + allocation: AllocationType + + @ManyToMany(() => Product, { cascade: true }) + @JoinTable({ + name: "discount_rule_products", + joinColumn: { + name: "discount_rule_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "product_id", + referencedColumnName: "id", + }, + }) + valid_for: Product[] + + @Column({ nullable: true }) + usage_limit: number + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + const id = ulid() + this.id = `dru_${id}` + } +} diff --git a/packages/medusa/src/models/discount.js b/packages/medusa/src/models/discount.js deleted file mode 100644 index b67f2c7571..0000000000 --- a/packages/medusa/src/models/discount.js +++ /dev/null @@ -1,24 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" -import DiscountRule from "./schemas/discount-rule" - -class DiscountModel extends BaseModel { - static modelName = "Discount" - - static schema = { - code: { type: String, required: true, unique: true }, - is_dynamic: { type: Boolean, default: false }, - is_giftcard: { type: Boolean, default: false }, - discount_rule: { type: DiscountRule, required: true }, - usage_count: { type: Number, default: 0 }, - disabled: { type: Boolean, default: false }, - starts_at: { type: Date }, - ends_at: { type: Date }, - regions: { type: [String], default: [] }, - original_amount: { type: Number }, - created: { type: String, default: Date.now }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default DiscountModel diff --git a/packages/medusa/src/models/discount.ts b/packages/medusa/src/models/discount.ts new file mode 100644 index 0000000000..969ea6215b --- /dev/null +++ b/packages/medusa/src/models/discount.ts @@ -0,0 +1,89 @@ +import { + Entity, + BeforeInsert, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + Index, + Column, + PrimaryColumn, + ManyToMany, + ManyToOne, + OneToOne, + JoinTable, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { DiscountRule } from "./discount-rule" +import { Region } from "./region" + +@Entity() +export class Discount { + @PrimaryColumn() + id: string + + @Index({ unique: true }) + @Column() + code: string + + @Column() + is_dynamic: boolean + + @Column({ nullable: true }) + rule_id: string + + @ManyToOne(() => DiscountRule, { cascade: true, eager: true }) + @JoinColumn({ name: "rule_id" }) + rule: DiscountRule + + @Column() + is_disabled: boolean + + @Column({ nullable: true }) + parent_discount_id: string + + @ManyToOne(() => Discount) + @JoinColumn({ name: "parent_discount_id" }) + parent_discount: Discount + + @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" }) + starts_at: Date + + @Column({ type: "timestamptz", nullable: true }) + ends_at: Date + + @ManyToMany(() => Region, { cascade: true }) + @JoinTable({ + name: "discount_regions", + joinColumn: { + name: "discount_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "region_id", + referencedColumnName: "id", + }, + }) + regions: Region[] + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `disc_${id}` + this.code = this.code.toUpperCase() + } +} diff --git a/packages/medusa/src/models/document.js b/packages/medusa/src/models/document.js deleted file mode 100644 index 2dc2356e02..0000000000 --- a/packages/medusa/src/models/document.js +++ /dev/null @@ -1,16 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class DocumentModel extends BaseModel { - static modelName = "Document" - - static schema = { - base_64: { type: String, required: true }, - name: { type: String, required: true }, - type: { type: String, required: true }, - created: { type: String, default: Date.now }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default DocumentModel diff --git a/packages/medusa/src/models/dynamic-discount-code.js b/packages/medusa/src/models/dynamic-discount-code.js deleted file mode 100644 index 1541a175e6..0000000000 --- a/packages/medusa/src/models/dynamic-discount-code.js +++ /dev/null @@ -1,16 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class DynamicDiscountCode extends BaseModel { - static modelName = "DynamicDiscountCode" - - static schema = { - code: { type: String, required: true, unique: true }, - discount_id: { type: String, required: true }, - usage_count: { type: Number, default: 0 }, - disabled: { type: Boolean, default: false }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default DynamicDiscountCode diff --git a/packages/medusa/src/models/fulfillment-item.ts b/packages/medusa/src/models/fulfillment-item.ts new file mode 100644 index 0000000000..3e18f650b4 --- /dev/null +++ b/packages/medusa/src/models/fulfillment-item.ts @@ -0,0 +1,40 @@ +import { + Entity, + Generated, + RelationId, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" + +import { Fulfillment } from "./fulfillment" +import { LineItem } from "./line-item" + +@Entity() +export class FulfillmentItem { + @PrimaryColumn() + fulfillment_id: string + + @PrimaryColumn() + item_id: string + + @ManyToOne(() => Fulfillment) + @JoinColumn({ name: "fulfillment_id" }) + fulfillment: Fulfillment + + @ManyToOne(() => LineItem) + @JoinColumn({ name: "item_id" }) + item: LineItem + + @Column({ type: "int" }) + quantity: number +} diff --git a/packages/medusa/src/models/fulfillment-provider.ts b/packages/medusa/src/models/fulfillment-provider.ts new file mode 100644 index 0000000000..bc9c84f929 --- /dev/null +++ b/packages/medusa/src/models/fulfillment-provider.ts @@ -0,0 +1,10 @@ +import { Entity, Column, PrimaryColumn, OneToOne, JoinColumn } from "typeorm" + +@Entity() +export class FulfillmentProvider { + @PrimaryColumn() + id: string + + @Column({ default: true }) + is_installed: boolean +} diff --git a/packages/medusa/src/models/fulfillment.ts b/packages/medusa/src/models/fulfillment.ts new file mode 100644 index 0000000000..0d9c2262cf --- /dev/null +++ b/packages/medusa/src/models/fulfillment.ts @@ -0,0 +1,93 @@ +import { + Entity, + RelationId, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Order } from "./order" +import { FulfillmentProvider } from "./fulfillment-provider" +import { FulfillmentItem } from "./fulfillment-item" +import { Swap } from "./swap" + +@Entity() +export class Fulfillment { + @PrimaryColumn() + id: string + + @Column({ nullable: true }) + swap_id: string + + @ManyToOne( + () => Swap, + swap => swap.fulfillments + ) + @JoinColumn({ name: "swap_id" }) + swap: Swap + + @Column({ nullable: true }) + order_id: string + + @ManyToOne( + () => Order, + o => o.fulfillments + ) + @JoinColumn({ name: "order_id" }) + order: Order + + @Column() + provider_id: string + + @ManyToOne(() => FulfillmentProvider) + @JoinColumn({ name: "provider_id" }) + provider: FulfillmentProvider + + @OneToMany( + () => FulfillmentItem, + i => i.fulfillment, + { eager: true, cascade: true } + ) + items: FulfillmentItem[] + + @Column({ type: "jsonb", default: [] }) + tracking_numbers: string[] + + @Column({ type: "jsonb" }) + data: any + + @Column({ type: "timestamptz", nullable: true }) + shipped_at: Date + + @Column({ type: "timestamptz", nullable: true }) + canceled_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `ful_${id}` + } +} diff --git a/packages/medusa/src/models/gift-card-transaction.ts b/packages/medusa/src/models/gift-card-transaction.ts new file mode 100644 index 0000000000..659c065063 --- /dev/null +++ b/packages/medusa/src/models/gift-card-transaction.ts @@ -0,0 +1,48 @@ +import { + Entity, + BeforeInsert, + CreateDateColumn, + Column, + PrimaryColumn, + ManyToOne, + Unique, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { GiftCard } from "./gift-card" +import { Order } from "./order" + +@Unique("gcuniq", ["gift_card_id", "order_id"]) +@Entity() +export class GiftCardTransaction { + @PrimaryColumn() + id: string + + @Column() + gift_card_id: string + + @ManyToOne(() => GiftCard) + @JoinColumn({ name: "gift_card_id" }) + gift_card: GiftCard + + @Column() + order_id: string + + @ManyToOne(() => Order) + @JoinColumn({ name: "order_id" }) + order: Order + + @Column("int") + amount: number + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `gct_${id}` + } +} diff --git a/packages/medusa/src/models/gift-card.ts b/packages/medusa/src/models/gift-card.ts new file mode 100644 index 0000000000..bacd83b2db --- /dev/null +++ b/packages/medusa/src/models/gift-card.ts @@ -0,0 +1,75 @@ +import { + Entity, + BeforeInsert, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + Index, + Column, + PrimaryColumn, + ManyToOne, + OneToOne, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Region } from "./region" +import { Order } from "./order" + +@Entity() +export class GiftCard { + @PrimaryColumn() + id: string + + @Index({ unique: true }) + @Column() + code: string + + @Column("int") + value: number + + @Column("int") + balance: number + + @Column() + region_id: string + + @ManyToOne(() => Region) + @JoinColumn({ name: "region_id" }) + region: Region + + @Column({ nullable: true }) + order_id: string + + @OneToOne(() => Order) + @JoinColumn({ name: "order_id" }) + order: Order + + @Column({ default: false }) + is_disabled: boolean + + @Column({ + type: "timestamptz", + nullable: true, + }) + ends_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `gift_${id}` + } +} diff --git a/packages/medusa/src/models/idempotency-key.ts b/packages/medusa/src/models/idempotency-key.ts new file mode 100644 index 0000000000..4bd6ba2ec5 --- /dev/null +++ b/packages/medusa/src/models/idempotency-key.ts @@ -0,0 +1,49 @@ +import { + Entity, + BeforeInsert, + CreateDateColumn, + Index, + Column, + PrimaryColumn, +} from "typeorm" +import { ulid } from "ulid" + +@Entity() +export class IdempotencyKey { + @PrimaryColumn() + id: string + + @Index({ unique: true }) + @Column() + idempotency_key: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @Column({ type: "timestamptz", nullable: true }) + locked_at: Date + + @Column({ nullable: true }) + request_method: string + + @Column({ type: "jsonb", nullable: true }) + request_params: any + + @Column({ nullable: true }) + request_path: string + + @Column({ type: "int", nullable: true }) + response_code: number + + @Column({ type: "jsonb", nullable: true }) + response_body: any + + @Column({ default: "started" }) + recovery_point: string + + @BeforeInsert() + private beforeInsert() { + const id = ulid() + this.id = `ikey_${id}` + } +} diff --git a/packages/medusa/src/models/image.ts b/packages/medusa/src/models/image.ts new file mode 100644 index 0000000000..b63ad2880e --- /dev/null +++ b/packages/medusa/src/models/image.ts @@ -0,0 +1,38 @@ +import { + Entity, + BeforeInsert, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + Column, + PrimaryColumn, +} from "typeorm" +import { ulid } from "ulid" + +@Entity() +export class Image { + @PrimaryColumn() + id: string + + @Column() + url: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `img_${id}` + } +} diff --git a/packages/medusa/src/models/line-item.ts b/packages/medusa/src/models/line-item.ts new file mode 100644 index 0000000000..dcf7c97364 --- /dev/null +++ b/packages/medusa/src/models/line-item.ts @@ -0,0 +1,123 @@ +import { + Entity, + BeforeInsert, + CreateDateColumn, + UpdateDateColumn, + Check, + Index, + Column, + PrimaryColumn, + ManyToOne, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Swap } from "./swap" +import { Cart } from "./cart" +import { Order } from "./order" +import { ProductVariant } from "./product-variant" + +@Check(`"fulfilled_quantity" <= "quantity"`) +@Check(`"shipped_quantity" <= "fulfilled_quantity"`) +@Check(`"returned_quantity" <= "quantity"`) +@Check(`"quantity" > 0`) +@Entity() +export class LineItem { + @PrimaryColumn() + id: string + + @Index() + @Column({ nullable: true }) + cart_id: string + + @ManyToOne( + () => Cart, + cart => cart.items + ) + @JoinColumn({ name: "cart_id" }) + cart: Cart + + @Index() + @Column({ nullable: true }) + order_id: string + + @ManyToOne( + () => Order, + order => order.items + ) + @JoinColumn({ name: "order_id" }) + order: Order + + @Index() + @Column({ nullable: true }) + swap_id: string + + @ManyToOne( + () => Swap, + swap => swap.additional_items + ) + @JoinColumn({ name: "swap_id" }) + swap: Swap + + @Column() + title: string + + @Column({ nullable: true }) + description: string + + @Column({ nullable: true }) + thumbnail: string + + @Column({ default: false }) + is_giftcard: boolean + + @Column({ default: true }) + should_merge: boolean + + @Column({ default: true }) + allow_discounts: boolean + + @Column({ nullable: true }) + has_shipping: boolean + + @Column({ type: "int" }) + unit_price: number + + @Index() + @Column({ nullable: true }) + variant_id: string + + @ManyToOne(() => ProductVariant, { eager: true }) + @JoinColumn({ name: "variant_id" }) + variant: ProductVariant + + @Column({ type: "int" }) + quantity: number + + @Column({ nullable: true, type: "int" }) + fulfilled_quantity: number + + @Column({ nullable: true, type: "int" }) + returned_quantity: number + + @Column({ nullable: true, type: "int" }) + shipped_quantity: number + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + refundable: number | null + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `item_${id}` + } +} diff --git a/packages/medusa/src/models/money-amount.ts b/packages/medusa/src/models/money-amount.ts new file mode 100644 index 0000000000..5b4cf12db3 --- /dev/null +++ b/packages/medusa/src/models/money-amount.ts @@ -0,0 +1,70 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + RelationId, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Currency } from "./currency" +import { ProductVariant } from "./product-variant" +import { Region } from "./region" + +@Entity() +export class MoneyAmount { + @PrimaryColumn() + id: string + + @Column() + currency_code: string + + @ManyToOne(() => Currency) + @JoinColumn({ name: "currency_code", referencedColumnName: "code" }) + currency: Currency + + @Column({ type: "int" }) + amount: number + + @Column({ type: "int", nullable: true, default: null }) + sale_amount: number + + @Column({ nullable: true }) + variant_id: string + + @ManyToOne(() => ProductVariant) + @JoinColumn({ name: "variant_id" }) + variant: ProductVariant + + @Column({ nullable: true }) + region_id: string + + @ManyToOne(() => Region) + @JoinColumn({ name: "region_id" }) + region: Region + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `ma_${id}` + } +} diff --git a/packages/medusa/src/models/oauth.js b/packages/medusa/src/models/oauth.js deleted file mode 100644 index 7729e8e87e..0000000000 --- a/packages/medusa/src/models/oauth.js +++ /dev/null @@ -1,16 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class OauthModel extends BaseModel { - static modelName = "Oauth" - - static schema = { - display_name: { type: String, required: true }, - application_name: { type: String, required: true, unique: true }, - install_url: { type: String, required: true }, - uninstall_url: { type: String, default: "" }, - data: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default OauthModel diff --git a/packages/medusa/src/models/oauth.ts b/packages/medusa/src/models/oauth.ts new file mode 100644 index 0000000000..d39828d677 --- /dev/null +++ b/packages/medusa/src/models/oauth.ts @@ -0,0 +1,47 @@ +import { + Entity, + Index, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + RelationId, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +@Entity() +export class Oauth { + @PrimaryColumn() + id: string + + @Column() + display_name: string + + @Index({ unique: true }) + @Column() + application_name: string + + @Column({ nullable: true }) + install_url: string + + @Column({ nullable: true }) + uninstall_url: string + + @Column({ type: "jsonb", nullable: true }) + data: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `oauth_${id}` + } +} diff --git a/packages/medusa/src/models/order.js b/packages/medusa/src/models/order.js deleted file mode 100644 index 2efbef7931..0000000000 --- a/packages/medusa/src/models/order.js +++ /dev/null @@ -1,46 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import LineItemSchema from "./schemas/line-item" -import PaymentMethodSchema from "./schemas/payment-method" -import ShippingMethodSchema from "./schemas/shipping-method" -import AddressSchema from "./schemas/address" -import DiscountSchema from "./schemas/discount" -import ReturnSchema from "./schemas/return" -import RefundSchema from "./schemas/refund" -import FulfillmentSchema from "./schemas/fulfillment" - -class OrderModel extends BaseModel { - static modelName = "Order" - - static schema = { - display_id: { type: String, required: true, unique: true }, - // pending, completed, archived, cancelled - status: { type: String, default: "pending" }, - // not_fulfilled, partially_fulfilled (some line items have been returned), fulfilled, returned, - fulfillment_status: { type: String, default: "not_fulfilled" }, - // awaiting, captured, refunded - payment_status: { type: String, default: "awaiting" }, - email: { type: String, required: true }, - cart_id: { type: String, unique: true, sparse: true }, - billing_address: { type: AddressSchema, required: true }, - shipping_address: { type: AddressSchema, required: true }, - items: { type: [LineItemSchema], required: true }, - currency_code: { type: String, required: true }, - tax_rate: { type: Number, required: true }, - fulfillments: { type: [FulfillmentSchema], default: [] }, - returns: { type: [ReturnSchema], default: [] }, - refunds: { type: [RefundSchema], default: [] }, - region_id: { type: String, required: true }, - discounts: { type: [DiscountSchema], default: [] }, - customer_id: { type: String }, - payment_method: { type: PaymentMethodSchema, required: true }, - shipping_methods: { type: [ShippingMethodSchema], required: true }, - swaps: { type: [String], default: [] }, - documents: { type: [String], default: [] }, - created: { type: String, default: Date.now }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default OrderModel diff --git a/packages/medusa/src/models/order.ts b/packages/medusa/src/models/order.ts new file mode 100644 index 0000000000..8d284d29c5 --- /dev/null +++ b/packages/medusa/src/models/order.ts @@ -0,0 +1,244 @@ +import { + Entity, + Generated, + BeforeInsert, + Column, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Address } from "./address" +import { LineItem } from "./line-item" +import { Currency } from "./currency" +import { Customer } from "./customer" +import { Region } from "./region" +import { Discount } from "./discount" +import { GiftCard } from "./gift-card" +import { GiftCardTransaction } from "./gift-card-transaction" +import { Payment } from "./payment" +import { Cart } from "./cart" +import { Fulfillment } from "./fulfillment" +import { Return } from "./return" +import { Refund } from "./refund" +import { Swap } from "./swap" +import { ShippingMethod } from "./shipping-method" + +export enum OrderStatus { + PENDING = "pending", + COMPLETED = "completed", + ARCHIVED = "archived", + CANCELED = "canceled", + REQUIRES_ACTION = "requires_action", +} + +export enum FulfillmentStatus { + NOT_FULFILLED = "not_fulfilled", + PARTIALLY_FULFILLED = "partially_fulfilled", + FULFILLED = "fulfilled", + PARTIALLY_SHIPPED = "partially_shipped", + SHIPPED = "shipped", + PARTIALLY_RETURNED = "partially_returned", + RETURNED = "returned", + CANCELED = "canceled", + REQUIRES_ACTION = "requires_action", +} + +export enum PaymentStatus { + NOT_PAID = "not_paid", + AWAITING = "awaiting", + CAPTURED = "captured", + PARTIALLY_REFUNDED = "partially_refunded", + REFUNDED = "refunded", + CANCELED = "canceled", + REQUIRES_ACTION = "requires_action", +} + +@Entity() +export class Order { + @PrimaryColumn() + id: string + + @Column({ type: "enum", enum: OrderStatus, default: "pending" }) + status: OrderStatus + + @Column({ type: "enum", enum: FulfillmentStatus, default: "not_fulfilled" }) + fulfillment_status: FulfillmentStatus + + @Column({ type: "enum", enum: PaymentStatus, default: "not_paid" }) + payment_status: PaymentStatus + + @Column() + @Generated("increment") + display_id: number + + @Column({ nullable: true }) + cart_id: string + + @OneToOne(() => Cart) + @JoinColumn({ name: "cart_id" }) + cart: Cart + + @Column() + customer_id: string + + @ManyToOne(() => Customer, { cascade: ["insert"] }) + @JoinColumn({ name: "customer_id" }) + customer: Customer + + @Column() + email: string + + @Column({ nullable: true }) + billing_address_id: string + + @ManyToOne(() => Address, { cascade: ["insert"] }) + @JoinColumn({ name: "billing_address_id" }) + billing_address: Address + + @Column({ nullable: true }) + shipping_address_id: string + + @ManyToOne(() => Address, { cascade: ["insert"] }) + @JoinColumn({ name: "shipping_address_id" }) + shipping_address: Address + + @Column() + region_id: string + + @ManyToOne(() => Region) + @JoinColumn({ name: "region_id" }) + region: Region + + @Column() + currency_code: string + + @ManyToOne(() => Currency) + @JoinColumn({ name: "currency_code", referencedColumnName: "code" }) + currency: Currency + + @Column({ type: "int" }) + tax_rate: number + + @ManyToMany(() => Discount, { cascade: ["insert"] }) + @JoinTable({ + name: "order_discounts", + joinColumn: { + name: "order_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "discount_id", + referencedColumnName: "id", + }, + }) + discounts: Discount[] + + @ManyToMany(() => GiftCard, { cascade: ["insert"] }) + @JoinTable({ + name: "order_gift_cards", + joinColumn: { + name: "order_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "gift_card_id", + referencedColumnName: "id", + }, + }) + gift_cards: GiftCard[] + + @OneToMany( + () => ShippingMethod, + method => method.order, + { cascade: ["insert"] } + ) + shipping_methods: ShippingMethod[] + + @OneToMany( + () => Payment, + payment => payment.order, + { cascade: ["insert"] } + ) + payments: Payment[] + + @OneToMany( + () => Fulfillment, + fulfillment => fulfillment.order, + { cascade: ["insert"] } + ) + fulfillments: Fulfillment[] + + @OneToMany( + () => Return, + ret => ret.order, + { cascade: ["insert"] } + ) + returns: Return[] + + @OneToMany( + () => Refund, + ref => ref.order, + { cascade: ["insert"] } + ) + refunds: Refund[] + + @OneToMany( + () => Swap, + swap => swap.order, + { cascade: ["insert"] } + ) + swaps: Swap[] + + @OneToMany( + () => LineItem, + lineItem => lineItem.order, + { cascade: ["insert"] } + ) + items: LineItem[] + + @OneToMany( + () => GiftCardTransaction, + gc => gc.order + ) + gift_card_transactions: GiftCardTransaction[] + + @Column({ nullable: true, type: "timestamptz" }) + canceled_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + // Total fields + shipping_total: number + discount_total: number + tax_total: number + refunded_total: number + total: number + subtotal: number + refundable_amount: number + gift_card_total: number + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `order_${id}` + } +} diff --git a/packages/medusa/src/models/payment-provider.ts b/packages/medusa/src/models/payment-provider.ts new file mode 100644 index 0000000000..b68dc9f23e --- /dev/null +++ b/packages/medusa/src/models/payment-provider.ts @@ -0,0 +1,10 @@ +import { Entity, Column, PrimaryColumn, OneToOne, JoinColumn } from "typeorm" + +@Entity() +export class PaymentProvider { + @PrimaryColumn() + id: string + + @Column({ default: true }) + is_installed: boolean +} diff --git a/packages/medusa/src/models/payment-session.ts b/packages/medusa/src/models/payment-session.ts new file mode 100644 index 0000000000..18d9b95d33 --- /dev/null +++ b/packages/medusa/src/models/payment-session.ts @@ -0,0 +1,66 @@ +import { + Entity, + CreateDateColumn, + UpdateDateColumn, + BeforeInsert, + Column, + PrimaryColumn, + ManyToOne, + JoinColumn, + Unique, +} from "typeorm" +import {ulid } from "ulid" +import { Cart } from "./cart" + +export enum PaymentSessionStatus { + AUTHORIZED = "authorized", + PENDING = "pending", + REQUIRES_MORE = "requires_more", + ERROR = "error", + CANCELED = "canceled", +} + +@Unique("OneSelected", ["cart_id", "is_selected"]) +@Entity() +export class PaymentSession { + @PrimaryColumn() + id: string + + @Column() + cart_id: string + + @ManyToOne( + () => Cart, + cart => cart.payment_sessions + ) + @JoinColumn({ name: "cart_id" }) + cart: Cart + + @Column() + provider_id: string + + @Column({ nullable: true }) + is_selected: boolean + + @Column({ type: "enum", enum: PaymentSessionStatus }) + status: string + + @Column({ type: "jsonb" }) + data: any + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `ps_${id}` + } +} diff --git a/packages/medusa/src/models/payment.ts b/packages/medusa/src/models/payment.ts new file mode 100644 index 0000000000..c9677e0648 --- /dev/null +++ b/packages/medusa/src/models/payment.ts @@ -0,0 +1,91 @@ +import { + Entity, + BeforeInsert, + Column, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + ManyToOne, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Swap } from "./swap" +import { Currency } from "./currency" +import { Cart } from "./cart" +import { Order } from "./order" + +@Entity() +export class Payment { + @PrimaryColumn() + id: string + + @Column({ nullable: true }) + swap_id: string + + @OneToOne(() => Swap) + @JoinColumn({ name: "swap_id" }) + swap: Swap + + @Column({ nullable: true }) + cart_id: string + + @OneToOne(() => Cart) + @JoinColumn({ name: "cart_id" }) + cart: Cart + + @Column({ nullable: true }) + order_id: string + + @ManyToOne( + () => Order, + order => order.payments + ) + @JoinColumn({ name: "order_id" }) + order: Order + + @Column({ type: "int" }) + amount: number + + @Column() + currency_code: string + + @ManyToOne(() => Currency) + @JoinColumn({ name: "currency_code", referencedColumnName: "code" }) + currency: Currency + + @Column({ type: "int", default: 0 }) + amount_refunded: number + + @Column() + provider_id: string + + @Column({ type: "jsonb" }) + data: any + + @Column({ type: "timestamptz", nullable: true }) + captured_at: Date + + @Column({ type: "timestamptz", nullable: true }) + canceled_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `pay_${id}` + } +} diff --git a/packages/medusa/src/models/product-option-value.ts b/packages/medusa/src/models/product-option-value.ts new file mode 100644 index 0000000000..251e343308 --- /dev/null +++ b/packages/medusa/src/models/product-option-value.ts @@ -0,0 +1,65 @@ +import { + Entity, + Index, + JoinColumn, + BeforeInsert, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + Column, + PrimaryColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { ProductOption } from "./product-option" +import { ProductVariant } from "./product-variant" + +@Entity() +export class ProductOptionValue { + @PrimaryColumn() + id: string + + @Column() + value: string + + @Index() + @Column() + option_id: string + + @ManyToOne( + () => ProductOption, + option => option.values + ) + @JoinColumn({ name: "option_id" }) + option: ProductOption + + @Column() + variant_id: string + + @ManyToOne( + () => ProductVariant, + variant => variant.options + ) + @JoinColumn({ name: "variant_id" }) + variant: ProductVariant + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `optval_${id}` + } +} diff --git a/packages/medusa/src/models/product-option.ts b/packages/medusa/src/models/product-option.ts new file mode 100644 index 0000000000..d908ba159b --- /dev/null +++ b/packages/medusa/src/models/product-option.ts @@ -0,0 +1,57 @@ +import { + Entity, + BeforeInsert, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + OneToMany, + Column, + PrimaryColumn, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Product } from "./product" +import { ProductOptionValue } from "./product-option-value" + +@Entity() +export class ProductOption { + @PrimaryColumn() + id: string + + @Column() + title: string + + @OneToMany( + () => ProductOptionValue, + value => value.option + ) + values: ProductOptionValue + + @ManyToOne( + () => Product, + product => product.options + ) + @JoinColumn({ name: "product_id" }) + product: Product + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `opt_${id}` + } +} diff --git a/packages/medusa/src/models/product-variant.js b/packages/medusa/src/models/product-variant.js deleted file mode 100644 index 4eae720430..0000000000 --- a/packages/medusa/src/models/product-variant.js +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************* - * models/product-variant.js - * - ******************************************************************************/ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import MoneyAmountSchema from "./schemas/money-amount" -import OptionValueSchema from "./schemas/option-value" - -class ProductVariantModel extends BaseModel { - static modelName = "ProductVariant" - static schema = { - title: { type: String, required: true }, - prices: { type: [MoneyAmountSchema], default: [], required: true }, - sku: { type: String, default: "" }, - barcode: { type: String, default: "" }, - options: { type: [OptionValueSchema], default: [] }, - sku: { type: String, unique: true, sparse: true }, - ean: { type: String, unique: true, sparse: true }, - image: { type: String, default: "" }, - published: { type: Boolean, default: false }, - inventory_quantity: { type: Number, default: 0 }, - allow_backorder: { type: Boolean, default: false }, - manage_inventory: { type: Boolean, default: true }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default ProductVariantModel diff --git a/packages/medusa/src/models/product-variant.ts b/packages/medusa/src/models/product-variant.ts new file mode 100644 index 0000000000..4a2d23192e --- /dev/null +++ b/packages/medusa/src/models/product-variant.ts @@ -0,0 +1,122 @@ +import { + Entity, + Index, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Product } from "./product" +import { MoneyAmount } from "./money-amount" +import { ProductOptionValue } from "./product-option-value" + +@Entity() +export class ProductVariant { + @PrimaryColumn() + id: string + + @Column() + title: string + + @Column() + product_id: string + + @ManyToOne( + () => Product, + product => product.variants, + { eager: true } + ) + @JoinColumn({ name: "product_id" }) + product: Product + + @OneToMany( + () => MoneyAmount, + ma => ma.variant, + { cascade: true } + ) + prices: MoneyAmount[] + + @Column({ nullable: true }) + @Index({ unique: true }) + sku: string + + @Index({ unique: true }) + @Column({ nullable: true }) + barcode: string + + @Index({ unique: true }) + @Column({ nullable: true }) + ean: string + + @Index({ unique: true }) + @Column({ nullable: true }) + upc: string + + @Column({ type: "int" }) + inventory_quantity: number + + @Column({ default: false }) + allow_backorder: boolean + + @Column({ default: true }) + manage_inventory: boolean + + @Column({ nullable: true }) + hs_code: string + + @Column({ nullable: true }) + origin_country: string + + @Column({ nullable: true }) + mid_code: string + + @Column({ nullable: true }) + material: string + + @Column({ type: "int", nullable: true }) + weight: number + + @Column({ type: "int", nullable: true }) + length: number + + @Column({ type: "int", nullable: true }) + height: number + + @Column({ type: "int", nullable: true }) + width: number + + @OneToMany( + () => ProductOptionValue, + optionValue => optionValue.variant, + { cascade: true } + ) + options: ProductOptionValue[] + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `variant_${id}` + } +} diff --git a/packages/medusa/src/models/product.js b/packages/medusa/src/models/product.js deleted file mode 100644 index 0735958943..0000000000 --- a/packages/medusa/src/models/product.js +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * models/product.js - * - ******************************************************************************/ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import OptionSchema from "./schemas/option" - -class ProductModel extends BaseModel { - static modelName = "Product" - static schema = { - title: { type: String, required: true }, - description: { type: String, default: "" }, - tags: { type: String, default: "" }, - handle: { type: String, unique: true, sparse: true }, - is_giftcard: { type: Boolean, default: false }, - images: { type: [String], default: [] }, - thumbnail: { type: String, default: "" }, - options: { type: [OptionSchema], default: [] }, - variants: { type: [String], default: [] }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - published: { type: Boolean, default: false }, - } -} - -export default ProductModel diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts new file mode 100644 index 0000000000..9d71a174dd --- /dev/null +++ b/packages/medusa/src/models/product.ts @@ -0,0 +1,127 @@ +import { + Entity, + Index, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Image } from "./image" +import { ProductOption } from "./product-option" +import { ProductVariant } from "./product-variant" +import { ShippingProfile } from "./shipping-profile" + +@Entity() +export class Product { + @PrimaryColumn() + id: string + + @Column() + title: string + + @Column({ nullable: true }) + subtitle: string + + @Column({ nullable: true }) + description: string + + @Column({ nullable: true }) + tags: string + + @Index({ unique: true }) + @Column({ nullable: true }) + handle: string + + @Column({ default: false }) + is_giftcard: boolean + + @ManyToMany(() => Image) + @JoinTable({ + name: "product_images", + joinColumn: { + name: "product_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "image_id", + referencedColumnName: "id", + }, + }) + images: Image[] + + @Column({ nullable: true }) + thumbnail: string + + @OneToMany( + () => ProductOption, + productOption => productOption.product + ) + options: ProductOption[] + + @OneToMany( + () => ProductVariant, + variant => variant.product, + { cascade: true } + ) + variants: ProductVariant[] + + @Column() + profile_id: string + + @ManyToOne(() => ShippingProfile) + @JoinColumn({ name: "profile_id" }) + profile: ShippingProfile + + @Column({ type: "int", nullable: true }) + weight: number + + @Column({ type: "int", nullable: true }) + length: number + + @Column({ type: "int", nullable: true }) + height: number + + @Column({ type: "int", nullable: true }) + width: number + + @Column({ nullable: true }) + hs_code: string + + @Column({ nullable: true }) + origin_country: string + + @Column({ nullable: true }) + mid_code: string + + @Column({ nullable: true }) + material: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `prod_${id}` + } +} diff --git a/packages/medusa/src/models/refund.ts b/packages/medusa/src/models/refund.ts new file mode 100644 index 0000000000..24d3f06222 --- /dev/null +++ b/packages/medusa/src/models/refund.ts @@ -0,0 +1,66 @@ +import { + Entity, + BeforeInsert, + Column, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + ManyToOne, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Currency } from "./currency" +import { Cart } from "./cart" +import { Order } from "./order" + +export enum RefundReason { + DISCOUNT = "discount", + RETURN = "return", + SWAP = "swap", + OTHER = "other", +} + +@Entity() +export class Refund { + @PrimaryColumn() + id: string + + @Column() + order_id: string + + @ManyToOne( + () => Order, + order => order.payments + ) + @JoinColumn({ name: "order_id" }) + order: Order + + @Column({ type: "int" }) + amount: number + + @Column({ nullable: true }) + note: string + + @Column({ type: "enum", enum: RefundReason }) + reason: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + const id = ulid() + this.id = `ref_${id}` + } +} diff --git a/packages/medusa/src/models/region.js b/packages/medusa/src/models/region.js deleted file mode 100644 index 97ee74a03e..0000000000 --- a/packages/medusa/src/models/region.js +++ /dev/null @@ -1,18 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class RegionModel extends BaseModel { - static modelName = "Region" - static schema = { - name: { type: String, required: true }, - currency_code: { type: String, required: true }, - tax_rate: { type: Number, required: true, default: 0 }, - tax_code: { type: String }, - countries: { type: [String], default: [] }, - payment_providers: { type: [String], default: [] }, - fulfillment_providers: { type: [String], default: [] }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default RegionModel diff --git a/packages/medusa/src/models/region.ts b/packages/medusa/src/models/region.ts new file mode 100644 index 0000000000..eb244cb334 --- /dev/null +++ b/packages/medusa/src/models/region.ts @@ -0,0 +1,95 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + ManyToMany, + ManyToOne, + OneToMany, + JoinTable, + JoinColumn, +} from "typeorm" +import { ulid } from "ulid" + +import { Currency } from "./currency" +import { Country } from "./country" +import { PaymentProvider } from "./payment-provider" +import { FulfillmentProvider } from "./fulfillment-provider" + +@Entity() +export class Region { + @PrimaryColumn() + id: string + + @Column() + name: string + + @Column() + currency_code: string + + @ManyToOne(() => Currency) + @JoinColumn({ name: "currency_code", referencedColumnName: "code" }) + currency: Currency + + @Column({ type: "decimal" }) + tax_rate: number + + @Column({ nullable: true }) + tax_code: string + + @OneToMany( + () => Country, + c => c.region + ) + countries: Country[] + + @ManyToMany(() => PaymentProvider, { eager: true, cascade: true }) + @JoinTable({ + name: "region_payment_providers", + joinColumn: { + name: "region_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "provider_id", + referencedColumnName: "id", + }, + }) + payment_providers: PaymentProvider[] + + @ManyToMany(() => FulfillmentProvider, { eager: true, cascade: true }) + @JoinTable({ + name: "region_fulfillment_providers", + joinColumn: { + name: "region_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "provider_id", + referencedColumnName: "id", + }, + }) + fulfillment_providers: FulfillmentProvider[] + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `reg_${id}` + } +} diff --git a/packages/medusa/src/models/return-item.ts b/packages/medusa/src/models/return-item.ts new file mode 100644 index 0000000000..35e913534a --- /dev/null +++ b/packages/medusa/src/models/return-item.ts @@ -0,0 +1,52 @@ +import { + Entity, + Generated, + RelationId, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" + +import { Return } from "./return" +import { LineItem } from "./line-item" + +@Entity() +export class ReturnItem { + @PrimaryColumn() + return_id: string + + @PrimaryColumn() + item_id: string + + @ManyToOne(() => Return) + @JoinColumn({ name: "return_id" }) + return_order: Return + + @ManyToOne(() => LineItem) + @JoinColumn({ name: "item_id" }) + item: LineItem + + @Column({ type: "int" }) + quantity: number + + @Column({ type: "boolean", default: true }) + is_requested: boolean + + @Column({ type: "int", nullable: true }) + requested_quantity: number + + @Column({ type: "int", nullable: true }) + received_quantity: number + + @Column({ type: "jsonb", nullable: true }) + metadata: any +} diff --git a/packages/medusa/src/models/return.ts b/packages/medusa/src/models/return.ts new file mode 100644 index 0000000000..a98d7d518a --- /dev/null +++ b/packages/medusa/src/models/return.ts @@ -0,0 +1,99 @@ +import { + Entity, + RelationId, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Order } from "./order" +import { Swap } from "./swap" +import { ReturnItem } from "./return-item" +import { ShippingMethod } from "./shipping-method" + +export enum ReturnStatus { + REQUESTED = "requested", + RECEIVED = "received", + REQUIRES_ACTION = "requires_action", +} + +@Entity() +export class Return { + @PrimaryColumn() + id: string + + @Column({ type: "enum", enum: ReturnStatus, default: ReturnStatus.REQUESTED }) + status: ReturnStatus + + @OneToMany( + () => ReturnItem, + item => item.return_order, + { eager: true, cascade: ["insert"] } + ) + items: ReturnItem[] + + @Column({ nullable: true }) + swap_id: string + + @OneToOne( + () => Swap, + swap => swap.return_order + ) + @JoinColumn({ name: "swap_id" }) + swap: Swap + + @Column({ nullable: true }) + order_id: string + + @ManyToOne( + () => Order, + o => o.returns + ) + @JoinColumn({ name: "order_id" }) + order: Order + + @OneToOne( + () => ShippingMethod, + method => method.return_order, + { eager: true, cascade: true } + ) + shipping_method: ShippingMethod + + @Column({ type: "jsonb", nullable: true }) + shipping_data: any + + @Column({ type: "int" }) + refund_amount: number + + @Column({ type: "timestamptz", nullable: true }) + received_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `ret_${id}` + } +} diff --git a/packages/medusa/src/models/schemas/address.js b/packages/medusa/src/models/schemas/address.js deleted file mode 100644 index 1d804bc816..0000000000 --- a/packages/medusa/src/models/schemas/address.js +++ /dev/null @@ -1,17 +0,0 @@ -/******************************************************************************* - * - ******************************************************************************/ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - first_name: { type: String }, - last_name: { type: String }, - address_1: { type: String }, - address_2: { type: String }, - city: { type: String }, - country_code: { type: String }, - province: { type: String }, - postal_code: { type: String }, - phone: { type: String }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/discount-rule.js b/packages/medusa/src/models/schemas/discount-rule.js deleted file mode 100644 index b508f7a669..0000000000 --- a/packages/medusa/src/models/schemas/discount-rule.js +++ /dev/null @@ -1,28 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - description: { type: String }, - // Fixed, percentage or free shipping is allowed as type. - // The fixed discount type can be used as normal coupon code, giftcards, - // store credits and possibly more. - // The percentage discount type can be used as normal coupon code, giftcards - // and possibly more. - // The free shipping discount type is used only to give free shipping. - type: { type: String, required: true }, // Fixed, percent, free shipping - // The value is simply the amount of discount a customer or user will have. - // This depends on the type above, since percentage can not be above 100. - value: { type: Number, required: true }, - // This is either total, meaning that the discount will be applied to the - // total price of the cart - // or item, meaning that the discount can be applied to the product variants - // in the valid_for array. Lastly the allocation is completely ignored if - // discount type is free shipping. - allocation: { type: String, required: true }, - // Id's of product variants. Depends on allocation. - // If total is set, then the valid_for will not be used for anything, - // since the discount will work for the cart total. Else if item allocation - // is chosen, then we will go through the cart and apply the coupon code to - // all the valid product variants. - valid_for: { type: [String], default: [] }, - usage_limit: { type: Number }, -}) diff --git a/packages/medusa/src/models/schemas/discount.js b/packages/medusa/src/models/schemas/discount.js deleted file mode 100644 index c0fc8b96e2..0000000000 --- a/packages/medusa/src/models/schemas/discount.js +++ /dev/null @@ -1,15 +0,0 @@ -import mongoose from "mongoose" - -import DiscountRule from "./discount-rule" - -export default new mongoose.Schema({ - is_giftcard: { type: Boolean }, - code: { type: String }, - discount_rule: { type: DiscountRule }, - usage_count: { type: Number }, - disabled: { type: Boolean }, - starts_at: { type: Date }, - ends_at: { type: Date }, - regions: { type: [String], default: [] }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/fulfillment.js b/packages/medusa/src/models/schemas/fulfillment.js deleted file mode 100644 index 327534e190..0000000000 --- a/packages/medusa/src/models/schemas/fulfillment.js +++ /dev/null @@ -1,13 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - created: { type: String, default: Date.now }, - provider_id: { type: String, required: true }, - items: { type: [mongoose.Schema.Types.Mixed], required: true }, - data: { type: mongoose.Schema.Types.Mixed, default: {} }, - tracking_numbers: { type: [String], default: [] }, - shipped_at: { type: String }, - is_canceled: { type: Boolean, default: false }, - documents: { type: [String], default: [] }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js deleted file mode 100644 index 453373d0dd..0000000000 --- a/packages/medusa/src/models/schemas/line-item.js +++ /dev/null @@ -1,53 +0,0 @@ -/******************************************************************************* - * - ******************************************************************************/ -import mongoose from "mongoose" - -/** - * REMEMBER: When updating this line you must also update the LineItemService's - * validate method too. Otherwise we cannot copy lines directly. - */ -export default new mongoose.Schema( - { - title: { type: String, required: true }, - description: { type: String }, - thumbnail: { type: String }, - is_giftcard: { type: Boolean, default: false }, - should_merge: { type: Boolean, default: true }, - has_shipping: { type: Boolean, default: false }, - no_discount: { type: Boolean, default: false }, - - // mongoose doesn't allow multi-type validation but this field allows both - // an object containing: - // { - // unit_price: (MoneyAmount), - // variant: (ProductVariantSchema), - // product: (ProductSchema) - // } - // - // and and array containing: - // [ - // { - // unit_price: (MoneyAmount), - // quantity: (Number), - // variant: (ProductVariantSchema), - // product: (ProductSchema) - // } - // ] - // validation is done in the cart service. - // - // The unit_price field can be used to override the default pricing mechanism. - // By default the price will be set based on the variant(s) in content, - // however, to allow line items with variable pricing e.g. limited sales, gift - // cards etc. the unit_price field is provided to give more granular control. - content: { type: mongoose.Schema.Types.Mixed, required: true }, - quantity: { type: Number, required: true }, - returned: { type: Boolean, default: false }, - fulfilled: { type: Boolean, default: false }, - fulfilled_quantity: { type: Number, default: 0 }, - returned_quantity: { type: Number, default: 0 }, - shipped_quantity: { type: Number, default: 0 }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - }, - { minimize: false } -) diff --git a/packages/medusa/src/models/schemas/option-value.js b/packages/medusa/src/models/schemas/option-value.js deleted file mode 100644 index 80431b984c..0000000000 --- a/packages/medusa/src/models/schemas/option-value.js +++ /dev/null @@ -1,6 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - option_id: { type: mongoose.Types.ObjectId, required: true }, - value: { type: String, required: true }, -}) diff --git a/packages/medusa/src/models/schemas/option.js b/packages/medusa/src/models/schemas/option.js deleted file mode 100644 index 2cdce004fa..0000000000 --- a/packages/medusa/src/models/schemas/option.js +++ /dev/null @@ -1,10 +0,0 @@ -/******************************************************************************* - * models/option.js - * - ******************************************************************************/ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - title: { type: String, required: true }, - values: { type: [String], default: [] }, -}) diff --git a/packages/medusa/src/models/schemas/payment-method.js b/packages/medusa/src/models/schemas/payment-method.js deleted file mode 100644 index 642cc5d76b..0000000000 --- a/packages/medusa/src/models/schemas/payment-method.js +++ /dev/null @@ -1,9 +0,0 @@ -/******************************************************************************* - * - ******************************************************************************/ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - provider_id: { type: String }, - data: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/refund.js b/packages/medusa/src/models/schemas/refund.js deleted file mode 100644 index d75d2c9297..0000000000 --- a/packages/medusa/src/models/schemas/refund.js +++ /dev/null @@ -1,9 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - created: { type: String, default: Date.now }, - note: { type: String, default: "" }, - reason: { type: String, default: "" }, - amount: { type: Number, required: true }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/return-line-item.js b/packages/medusa/src/models/schemas/return-line-item.js deleted file mode 100644 index bfceac0219..0000000000 --- a/packages/medusa/src/models/schemas/return-line-item.js +++ /dev/null @@ -1,23 +0,0 @@ -/******************************************************************************* - * - ******************************************************************************/ -import mongoose from "mongoose" - -/** - * @typedef ReturnLineItem - * @property {String} item_id - * @property {Object} content - * @property {Number} quantity - * @property {Boolean} is_requested - * @property {Boolean} is_registered - * @property {Object} metadata - */ -export default new mongoose.Schema({ - item_id: { type: String, required: true }, - content: { type: mongoose.Schema.Types.Mixed, required: true }, - quantity: { type: Number, required: true }, - is_requested: { type: Boolean, required: true }, - requested_quantity: { type: Number }, - is_registered: { type: Boolean, default: false }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/return.js b/packages/medusa/src/models/schemas/return.js deleted file mode 100644 index a261f1f58c..0000000000 --- a/packages/medusa/src/models/schemas/return.js +++ /dev/null @@ -1,15 +0,0 @@ -import mongoose from "mongoose" -import ReturnLineItemSchema from "./return-line-item" -import ShippingMethodSchema from "./shipping-method" - -export default new mongoose.Schema({ - status: { type: String, default: "requested" }, - refund_amount: { type: Number, required: true }, - items: { type: [ReturnLineItemSchema], required: true }, - shipping_method: { type: ShippingMethodSchema, default: {} }, - shipping_data: { type: mongoose.Schema.Types.Mixed, default: {} }, - documents: { type: [String], default: [] }, - received_at: { type: String }, - created: { type: String, default: Date.now }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/shipment.js b/packages/medusa/src/models/schemas/shipment.js deleted file mode 100644 index e21b5dc924..0000000000 --- a/packages/medusa/src/models/schemas/shipment.js +++ /dev/null @@ -1,7 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - item_ids: { type: [String], required: true }, - tracking_number: { type: String, default: "" }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, -}) diff --git a/packages/medusa/src/models/schemas/shipping-method.js b/packages/medusa/src/models/schemas/shipping-method.js deleted file mode 100644 index 92a195676e..0000000000 --- a/packages/medusa/src/models/schemas/shipping-method.js +++ /dev/null @@ -1,10 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - name: { type: String, default: "" }, - provider_id: { type: String, required: true }, - profile_id: { type: String, required: true }, - price: { type: Number, required: true }, - data: { type: mongoose.Schema.Types.Mixed, default: {} }, - items: { type: [mongoose.Schema.Types.Mixed], default: [] }, -}) diff --git a/packages/medusa/src/models/schemas/shipping-option-price.js b/packages/medusa/src/models/schemas/shipping-option-price.js deleted file mode 100644 index 389f44d6af..0000000000 --- a/packages/medusa/src/models/schemas/shipping-option-price.js +++ /dev/null @@ -1,6 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - type: { type: String, required: true }, - amount: { type: Number }, -}) diff --git a/packages/medusa/src/models/schemas/shipping-option-requirement.js b/packages/medusa/src/models/schemas/shipping-option-requirement.js deleted file mode 100644 index 05881c9929..0000000000 --- a/packages/medusa/src/models/schemas/shipping-option-requirement.js +++ /dev/null @@ -1,6 +0,0 @@ -import mongoose from "mongoose" - -export default new mongoose.Schema({ - type: { type: String, required: true }, - value: { type: Number, required: true }, -}) diff --git a/packages/medusa/src/models/shipping-method.ts b/packages/medusa/src/models/shipping-method.ts new file mode 100644 index 0000000000..089ca17828 --- /dev/null +++ b/packages/medusa/src/models/shipping-method.ts @@ -0,0 +1,84 @@ +import { + Entity, + Check, + BeforeInsert, + Column, + PrimaryColumn, + ManyToOne, + OneToOne, + JoinColumn, + Index, +} from "typeorm" +import { ulid } from "ulid" + +import { Order } from "./order" +import { Cart } from "./cart" +import { Swap } from "./swap" +import { Return } from "./return" +import { ShippingOption } from "./shipping-option" + +@Check( + `"order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL` +) +@Check(`"price" >= 0`) +@Entity() +export class ShippingMethod { + @PrimaryColumn() + id: string + + @Index() + @Column() + shipping_option_id: string + + @Index() + @Column({ nullable: true }) + order_id: string + + @ManyToOne(() => Order) + @JoinColumn({ name: "order_id" }) + order: Order + + @Index() + @Column({ nullable: true }) + cart_id: string + + @ManyToOne(() => Cart) + @JoinColumn({ name: "cart_id" }) + cart: Cart + + @Index() + @Column({ nullable: true }) + swap_id: string + + @ManyToOne(() => Swap) + @JoinColumn({ name: "swap_id" }) + swap: Swap + + @Index() + @Column({ nullable: true }) + return_id: string + + @OneToOne( + () => Return, + ret => ret.shipping_method + ) + @JoinColumn({ name: "return_id" }) + return_order: Return + + @ManyToOne(() => ShippingOption, { eager: true }) + @JoinColumn({ name: "shipping_option_id" }) + shipping_option: ShippingOption + + @Column({ type: "int" }) + price: number + + @Column({ type: "jsonb" }) + data: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `sm_${id}` + } +} diff --git a/packages/medusa/src/models/shipping-option-requirement.ts b/packages/medusa/src/models/shipping-option-requirement.ts new file mode 100644 index 0000000000..c6c8331603 --- /dev/null +++ b/packages/medusa/src/models/shipping-option-requirement.ts @@ -0,0 +1,50 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + RelationId, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { ShippingOption } from "./shipping-option" + +export enum RequirementType { + MIN_SUBTOTAL = "min_subtotal", + MAX_SUBTOTAL = "max_subtotal", +} + +@Entity() +export class ShippingOptionRequirement { + @PrimaryColumn() + id: string + + @Column() + shipping_option_id: string + + @ManyToOne(() => ShippingOption) + @JoinColumn({ name: "shipping_option_id" }) + shipping_option: ShippingOption + + @Column({ type: "enum", enum: RequirementType }) + type: RequirementType + + @Column({ type: "int" }) + amount: number + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `sor_${id}` + } +} diff --git a/packages/medusa/src/models/shipping-option.js b/packages/medusa/src/models/shipping-option.js deleted file mode 100644 index c4d3c14d72..0000000000 --- a/packages/medusa/src/models/shipping-option.js +++ /dev/null @@ -1,22 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import ShippingOptionPrice from "./schemas/shipping-option-price" -import ShippingOptionRequirement from "./schemas/shipping-option-requirement" - -class ShippingOptionModel extends BaseModel { - static modelName = "ShippingOption" - static schema = { - name: { type: String, required: true }, - region_id: { type: String, required: true }, - provider_id: { type: String, required: true }, - profile_id: { type: String, required: true }, - data: { type: mongoose.Schema.Types.Mixed, default: {} }, - price: { type: ShippingOptionPrice, required: true }, - requirements: { type: [ShippingOptionRequirement], default: [] }, - is_return: { type: Boolean, default: false }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default ShippingOptionModel diff --git a/packages/medusa/src/models/shipping-option.ts b/packages/medusa/src/models/shipping-option.ts new file mode 100644 index 0000000000..9f2a746450 --- /dev/null +++ b/packages/medusa/src/models/shipping-option.ts @@ -0,0 +1,97 @@ +import { + Entity, + Check, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + RelationId, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { ShippingProfile } from "./shipping-profile" +import { Region } from "./region" +import { FulfillmentProvider } from "./fulfillment-provider" +import { ShippingOptionRequirement } from "./shipping-option-requirement" + +export enum ShippingOptionPriceType { + FLAT_RATE = "flat_rate", + CALCULATED = "calculated", +} + +@Check(`"amount" >= 0`) +@Entity() +export class ShippingOption { + @PrimaryColumn() + id: string + + @Column() + name: string + + @Column() + region_id: string + + @ManyToOne(() => Region) + @JoinColumn({ name: "region_id" }) + region: Region + + @Column() + profile_id: string + + @ManyToOne(() => ShippingProfile) + @JoinColumn({ name: "profile_id" }) + profile: ShippingProfile + + @Column() + provider_id: string + + @ManyToOne(() => FulfillmentProvider) + @JoinColumn({ name: "provider_id" }) + provider: FulfillmentProvider + + @Column({ type: "enum", enum: ShippingOptionPriceType }) + price_type: ShippingOptionPriceType + + @Column({ type: "int", nullable: true }) + amount: number + + @Column({ default: false }) + is_return: boolean + + @OneToMany( + () => ShippingOptionRequirement, + req => req.shipping_option, + { cascade: ["insert"] } + ) + requirements: ShippingOptionRequirement[] + + @Column({ type: "jsonb" }) + data: any + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `so_${id}` + } +} diff --git a/packages/medusa/src/models/shipping-profile.js b/packages/medusa/src/models/shipping-profile.js deleted file mode 100644 index d4da052720..0000000000 --- a/packages/medusa/src/models/shipping-profile.js +++ /dev/null @@ -1,13 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class ShippingProfileModel extends BaseModel { - static modelName = "ShippingProfile" - static schema = { - name: { type: String, required: true }, - products: { type: [String], default: [] }, - shipping_options: { type: [String], default: [] }, - } -} - -export default ShippingProfileModel diff --git a/packages/medusa/src/models/shipping-profile.ts b/packages/medusa/src/models/shipping-profile.ts new file mode 100644 index 0000000000..f5c22c34c4 --- /dev/null +++ b/packages/medusa/src/models/shipping-profile.ts @@ -0,0 +1,69 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + RelationId, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { ShippingOption } from "./shipping-option" +import { Product } from "./product" + +export enum ShippingProfileType { + DEFAULT = "default", + GIFT_CARD = "gift_card", + CUSTOM = "custom", +} + +@Entity() +export class ShippingProfile { + @PrimaryColumn() + id: string + + @Column() + name: string + + @Column({ type: "enum", enum: ShippingProfileType }) + type: ShippingProfileType + + @OneToMany( + () => Product, + product => product.profile + ) + products: Product[] + + @OneToMany( + () => ShippingOption, + so => so.profile + ) + shipping_options: ShippingOption[] + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `sp_${id}` + } +} diff --git a/packages/medusa/src/models/staged-job.ts b/packages/medusa/src/models/staged-job.ts new file mode 100644 index 0000000000..1b25615151 --- /dev/null +++ b/packages/medusa/src/models/staged-job.ts @@ -0,0 +1,35 @@ +import { + Entity, + RelationId, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +@Entity() +export class StagedJob { + @PrimaryColumn() + id: string + + @Column() + event_name: string + + @Column({ type: "jsonb" }) + data: any + + @BeforeInsert() + private beforeInsert() { + const id = ulid() + this.id = `job_${id}` + } +} diff --git a/packages/medusa/src/models/store.js b/packages/medusa/src/models/store.js deleted file mode 100644 index 0e1f7aa967..0000000000 --- a/packages/medusa/src/models/store.js +++ /dev/null @@ -1,17 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class StoreModel extends BaseModel { - static modelName = "Store" - static schema = { - name: { type: String, required: true, default: "Medusa Store" }, - default_currency: { type: String, required: true, default: "USD" }, - currencies: { type: [String], default: [] }, - payment_providers: { type: [String], default: [] }, - fulfillment_providers: { type: [String], default: [] }, - swap_link_template: { type: String }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default StoreModel diff --git a/packages/medusa/src/models/store.ts b/packages/medusa/src/models/store.ts new file mode 100644 index 0000000000..1b03e7e5e1 --- /dev/null +++ b/packages/medusa/src/models/store.ts @@ -0,0 +1,67 @@ +import { + Entity, + RelationId, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Currency } from "./currency" + +@Entity() +export class Store { + @PrimaryColumn() + id: string + + @Column({ default: "Medusa Store" }) + name: string + + @Column({ default: "usd" }) + default_currency_code: string + + @ManyToOne(() => Currency) + @JoinColumn({ name: "default_currency_code", referencedColumnName: "code" }) + default_currency: Currency + + @ManyToMany(() => Currency) + @JoinTable({ + name: "store_currencies", + joinColumn: { + name: "store_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "currency_code", + referencedColumnName: "code", + }, + }) + currencies: Currency[] + + @Column({ nullable: true }) + swap_link_template: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + const id = ulid() + this.id = `store_${id}` + } +} diff --git a/packages/medusa/src/models/swap.js b/packages/medusa/src/models/swap.js deleted file mode 100644 index 3b0710c803..0000000000 --- a/packages/medusa/src/models/swap.js +++ /dev/null @@ -1,36 +0,0 @@ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -import LineItemSchema from "./schemas/line-item" -import ReturnSchema from "./schemas/return" -import FulfillmentSchema from "./schemas/fulfillment" -import PaymentMethodSchema from "./schemas/payment-method" -import ShippingMethodSchema from "./schemas/shipping-method" -import AddressSchema from "./schemas/address" - -class SwapModel extends BaseModel { - static modelName = "Swap" - static schema = { - fulfillment_status: { type: String, default: "not_fulfilled" }, - payment_status: { type: String, default: "awaiting" }, - is_paid: { type: Boolean, default: false }, - return: { type: ReturnSchema }, - return_items: { type: [mongoose.Schema.Types.Mixed], required: true }, - return_shipping: { type: mongoose.Schema.Types.Mixed }, - fulfillments: { type: [FulfillmentSchema] }, - additional_items: { type: [LineItemSchema], required: true }, - payment_method: { type: PaymentMethodSchema }, - shipping_methods: { type: [ShippingMethodSchema] }, - shipping_address: { type: AddressSchema }, - amount_paid: { type: Number }, - region_id: { type: String }, - currency_code: { type: String }, - order_payment: { type: PaymentMethodSchema }, - order_id: { type: String }, - cart_id: { type: String }, - created: { type: String, default: Date.now }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default SwapModel diff --git a/packages/medusa/src/models/swap.ts b/packages/medusa/src/models/swap.ts new file mode 100644 index 0000000000..53f1b56f9e --- /dev/null +++ b/packages/medusa/src/models/swap.ts @@ -0,0 +1,143 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Order } from "./order" +import { Fulfillment } from "./fulfillment" +import { Address } from "./address" +import { LineItem } from "./line-item" +import { Return } from "./return" +import { Cart } from "./cart" +import { Payment } from "./payment" +import { ShippingMethod } from "./shipping-method" + +export enum FulfillmentStatus { + NOT_FULFILLED = "not_fulfilled", + FULFILLED = "fulfilled", + SHIPPED = "shipped", + CANCELED = "canceled", + REQUIRES_ACTION = "requires_action", +} + +export enum PaymentStatus { + NOT_PAID = "not_paid", + AWAITING = "awaiting", + CAPTURED = "captured", + CANCELED = "canceled", + DIFFERENCE_REFUNDED = "difference_refunded", + PARTIALLY_REFUNDED = "partially_refunded", + REFUNDED = "refunded", + REQUIRES_ACTION = "requires_action", +} + +@Entity() +export class Swap { + @PrimaryColumn() + id: string + + @Column({ type: "enum", enum: FulfillmentStatus }) + fulfillment_status: FulfillmentStatus + + @Column({ type: "enum", enum: PaymentStatus }) + payment_status: PaymentStatus + + @Column({ type: "string" }) + order_id: string + + @ManyToOne( + () => Order, + o => o.swaps + ) + @JoinColumn({ name: "order_id" }) + order: Order + + @OneToMany( + () => LineItem, + item => item.swap, + { cascade: ["insert"] } + ) + additional_items: LineItem + + @OneToOne( + () => Return, + ret => ret.swap, + { cascade: ["insert"] } + ) + return_order: Return + + @OneToMany( + () => Fulfillment, + fulfillment => fulfillment.swap, + { cascade: ["insert"] } + ) + fulfillments: Fulfillment[] + + @OneToOne( + () => Payment, + p => p.swap, + { cascade: ["insert"] } + ) + payment: Payment + + @Column({ type: "int", nullable: true }) + difference_due: number + + @Column({ nullable: true }) + shipping_address_id: string + + @ManyToOne(() => Address, { cascade: ["insert"] }) + @JoinColumn({ name: "shipping_address_id" }) + shipping_address: Address + + @OneToMany( + () => ShippingMethod, + method => method.swap, + { cascade: ["insert"] } + ) + shipping_methods: ShippingMethod[] + + @Column({ nullable: true }) + cart_id: string + + @OneToOne(() => Cart) + @JoinColumn({ name: "cart_id" }) + cart: Cart + + @Column({ type: "timestamptz", nullable: true }) + confirmed_at: Date + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `swap_${id}` + } +} diff --git a/packages/medusa/src/models/user.js b/packages/medusa/src/models/user.js deleted file mode 100644 index 41d43504c6..0000000000 --- a/packages/medusa/src/models/user.js +++ /dev/null @@ -1,19 +0,0 @@ -/******************************************************************************* - * models/user.js - * - ******************************************************************************/ -import mongoose from "mongoose" -import { BaseModel } from "medusa-interfaces" - -class UserModel extends BaseModel { - static modelName = "User" - static schema = { - email: { type: String, required: true }, - name: { type: String }, - password_hash: { type: String, required: true }, - api_token: { type: String }, - metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, - } -} - -export default UserModel diff --git a/packages/medusa/src/models/user.ts b/packages/medusa/src/models/user.ts new file mode 100644 index 0000000000..9251e001c4 --- /dev/null +++ b/packages/medusa/src/models/user.ts @@ -0,0 +1,52 @@ +import { + Entity, + BeforeInsert, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, + Column, + PrimaryColumn, +} from "typeorm" +import { ulid } from "ulid" + +@Entity() +export class User { + @PrimaryColumn() + id: string + + @Index({ unique: true }) + @Column() + email: string + + @Column({ nullable: true }) + first_name: string + + @Column({ nullable: true }) + last_name: string + + @Column() + password_hash: string + + @Column({ nullable: true }) + api_token: string + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `usr_${id}` + } +} diff --git a/packages/medusa/src/models/__mocks__/cart.js b/packages/medusa/src/repositories/__mocks__/cart.js similarity index 100% rename from packages/medusa/src/models/__mocks__/cart.js rename to packages/medusa/src/repositories/__mocks__/cart.js diff --git a/packages/medusa/src/models/__mocks__/customer.js b/packages/medusa/src/repositories/__mocks__/customer.js similarity index 100% rename from packages/medusa/src/models/__mocks__/customer.js rename to packages/medusa/src/repositories/__mocks__/customer.js diff --git a/packages/medusa/src/models/__mocks__/discount.js b/packages/medusa/src/repositories/__mocks__/discount.js similarity index 94% rename from packages/medusa/src/models/__mocks__/discount.js rename to packages/medusa/src/repositories/__mocks__/discount.js index 23594351d4..3fe1d0c0a9 100644 --- a/packages/medusa/src/models/__mocks__/discount.js +++ b/packages/medusa/src/repositories/__mocks__/discount.js @@ -5,7 +5,7 @@ export const discounts = { _id: IdMap.getId("dynamic"), code: "Something", is_dynamic: true, - discount_rule: { + rule: { type: "percentage", allocation: "total", value: 10, @@ -15,7 +15,7 @@ export const discounts = { total10Percent: { _id: IdMap.getId("total10"), code: "10%OFF", - discount_rule: { + rule: { type: "percentage", allocation: "total", value: 10, @@ -25,7 +25,7 @@ export const discounts = { item10Percent: { _id: IdMap.getId("item10Percent"), code: "MEDUSA", - discount_rule: { + rule: { type: "percentage", allocation: "item", value: 10, @@ -36,7 +36,7 @@ export const discounts = { total10Fixed: { _id: IdMap.getId("total10Fixed"), code: "MEDUSA", - discount_rule: { + rule: { type: "fixed", allocation: "total", value: 10, @@ -46,7 +46,7 @@ export const discounts = { item9Fixed: { _id: IdMap.getId("item9Fixed"), code: "MEDUSA", - discount_rule: { + rule: { type: "fixed", allocation: "item", value: 9, @@ -57,7 +57,7 @@ export const discounts = { item2Fixed: { _id: IdMap.getId("item2Fixed"), code: "MEDUSA", - discount_rule: { + rule: { type: "fixed", allocation: "item", value: 2, @@ -68,7 +68,7 @@ export const discounts = { item10FixedNoVariants: { _id: IdMap.getId("item10FixedNoVariants"), code: "MEDUSA", - discount_rule: { + rule: { type: "fixed", allocation: "item", value: 10, @@ -80,7 +80,7 @@ export const discounts = { _id: IdMap.getId("expired"), code: "MEDUSA", ends_at: new Date("December 17, 1995 03:24:00"), - discount_rule: { + rule: { type: "fixed", allocation: "item", value: 10, @@ -91,7 +91,7 @@ export const discounts = { freeShipping: { _id: IdMap.getId("freeshipping"), code: "FREESHIPPING", - discount_rule: { + rule: { type: "free_shipping", allocation: "total", value: 10, @@ -102,7 +102,7 @@ export const discounts = { USDiscount: { _id: IdMap.getId("us-discount"), code: "US10", - discount_rule: { + rule: { type: "free_shipping", allocation: "total", value: 10, @@ -112,7 +112,7 @@ export const discounts = { }, alreadyExists: { code: "ALREADYEXISTS", - discount_rule: { + rule: { type: "percentage", allocation: "total", value: 20, diff --git a/packages/medusa/src/models/__mocks__/document.js b/packages/medusa/src/repositories/__mocks__/document.js similarity index 100% rename from packages/medusa/src/models/__mocks__/document.js rename to packages/medusa/src/repositories/__mocks__/document.js diff --git a/packages/medusa/src/models/__mocks__/dynamic-discount-code.js b/packages/medusa/src/repositories/__mocks__/dynamic-discount-code.js similarity index 100% rename from packages/medusa/src/models/__mocks__/dynamic-discount-code.js rename to packages/medusa/src/repositories/__mocks__/dynamic-discount-code.js diff --git a/packages/medusa/src/models/__mocks__/order.js b/packages/medusa/src/repositories/__mocks__/order.js similarity index 100% rename from packages/medusa/src/models/__mocks__/order.js rename to packages/medusa/src/repositories/__mocks__/order.js diff --git a/packages/medusa/src/models/__mocks__/product-variant.js b/packages/medusa/src/repositories/__mocks__/product-variant.js similarity index 100% rename from packages/medusa/src/models/__mocks__/product-variant.js rename to packages/medusa/src/repositories/__mocks__/product-variant.js diff --git a/packages/medusa/src/models/__mocks__/product.js b/packages/medusa/src/repositories/__mocks__/product.js similarity index 100% rename from packages/medusa/src/models/__mocks__/product.js rename to packages/medusa/src/repositories/__mocks__/product.js diff --git a/packages/medusa/src/models/__mocks__/region.js b/packages/medusa/src/repositories/__mocks__/region.js similarity index 100% rename from packages/medusa/src/models/__mocks__/region.js rename to packages/medusa/src/repositories/__mocks__/region.js diff --git a/packages/medusa/src/models/__mocks__/shipping-option.js b/packages/medusa/src/repositories/__mocks__/shipping-option.js similarity index 100% rename from packages/medusa/src/models/__mocks__/shipping-option.js rename to packages/medusa/src/repositories/__mocks__/shipping-option.js diff --git a/packages/medusa/src/models/__mocks__/shipping-profile.js b/packages/medusa/src/repositories/__mocks__/shipping-profile.js similarity index 100% rename from packages/medusa/src/models/__mocks__/shipping-profile.js rename to packages/medusa/src/repositories/__mocks__/shipping-profile.js diff --git a/packages/medusa/src/models/__mocks__/store.js b/packages/medusa/src/repositories/__mocks__/store.js similarity index 100% rename from packages/medusa/src/models/__mocks__/store.js rename to packages/medusa/src/repositories/__mocks__/store.js diff --git a/packages/medusa/src/models/__mocks__/user.js b/packages/medusa/src/repositories/__mocks__/user.js similarity index 100% rename from packages/medusa/src/models/__mocks__/user.js rename to packages/medusa/src/repositories/__mocks__/user.js diff --git a/packages/medusa/src/repositories/address.ts b/packages/medusa/src/repositories/address.ts new file mode 100644 index 0000000000..ebe51b1f7e --- /dev/null +++ b/packages/medusa/src/repositories/address.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Address } from "../models/address" + +@EntityRepository(Address) +export class AddressRepository extends Repository
{ } diff --git a/packages/medusa/src/repositories/cart.ts b/packages/medusa/src/repositories/cart.ts new file mode 100644 index 0000000000..6955ad4895 --- /dev/null +++ b/packages/medusa/src/repositories/cart.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Cart } from "../models/cart" + +@EntityRepository(Cart) +export class CartRepository extends Repository {} diff --git a/packages/medusa/src/repositories/country.ts b/packages/medusa/src/repositories/country.ts new file mode 100644 index 0000000000..5f2772964f --- /dev/null +++ b/packages/medusa/src/repositories/country.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Country } from "../models/country" + +@EntityRepository(Country) +export class CountryRepository extends Repository { } diff --git a/packages/medusa/src/repositories/currency.ts b/packages/medusa/src/repositories/currency.ts new file mode 100644 index 0000000000..bf30c3e00b --- /dev/null +++ b/packages/medusa/src/repositories/currency.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Currency } from "../models/currency" + +@EntityRepository(Currency) +export class CurrencyRepository extends Repository { } diff --git a/packages/medusa/src/repositories/customer.ts b/packages/medusa/src/repositories/customer.ts new file mode 100644 index 0000000000..6a4838c740 --- /dev/null +++ b/packages/medusa/src/repositories/customer.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Customer } from "../models/customer" + +@EntityRepository(Customer) +export class CustomerRepository extends Repository {} diff --git a/packages/medusa/src/repositories/discount-rule.ts b/packages/medusa/src/repositories/discount-rule.ts new file mode 100644 index 0000000000..036a074392 --- /dev/null +++ b/packages/medusa/src/repositories/discount-rule.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { DiscountRule } from "../models/discount-rule" + +@EntityRepository(DiscountRule) +export class DiscountRuleRepository extends Repository { } diff --git a/packages/medusa/src/repositories/discount.ts b/packages/medusa/src/repositories/discount.ts new file mode 100644 index 0000000000..1aa5cf7067 --- /dev/null +++ b/packages/medusa/src/repositories/discount.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Discount } from "../models/discount" + +@EntityRepository(Discount) +export class DiscountRepository extends Repository {} diff --git a/packages/medusa/src/repositories/fulfillment-provider.ts b/packages/medusa/src/repositories/fulfillment-provider.ts new file mode 100644 index 0000000000..728af36e8d --- /dev/null +++ b/packages/medusa/src/repositories/fulfillment-provider.ts @@ -0,0 +1,7 @@ +import { EntityRepository, Repository } from "typeorm" +import { FulfillmentProvider } from "../models/fulfillment-provider" + +@EntityRepository(FulfillmentProvider) +export class FulfillmentProviderRepository extends Repository< + FulfillmentProvider +> {} diff --git a/packages/medusa/src/repositories/fulfillment.ts b/packages/medusa/src/repositories/fulfillment.ts new file mode 100644 index 0000000000..f56117603f --- /dev/null +++ b/packages/medusa/src/repositories/fulfillment.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Fulfillment } from "../models/fulfillment" + +@EntityRepository(Fulfillment) +export class FulfillmentRepository extends Repository {} diff --git a/packages/medusa/src/repositories/gift-card-transaction.ts b/packages/medusa/src/repositories/gift-card-transaction.ts new file mode 100644 index 0000000000..eab3d7306c --- /dev/null +++ b/packages/medusa/src/repositories/gift-card-transaction.ts @@ -0,0 +1,7 @@ +import { EntityRepository, Repository } from "typeorm" +import { GiftCardTransaction } from "../models/gift-card-transaction" + +@EntityRepository(GiftCardTransaction) +export class GiftCardTransactionRepository extends Repository< + GiftCardTransaction +> {} diff --git a/packages/medusa/src/repositories/gift-card.ts b/packages/medusa/src/repositories/gift-card.ts new file mode 100644 index 0000000000..578dc8ed58 --- /dev/null +++ b/packages/medusa/src/repositories/gift-card.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { GiftCard } from "../models/gift-card" + +@EntityRepository(GiftCard) +export class GiftCardRepository extends Repository {} diff --git a/packages/medusa/src/repositories/idempotency-key.ts b/packages/medusa/src/repositories/idempotency-key.ts new file mode 100644 index 0000000000..c17a2c58d7 --- /dev/null +++ b/packages/medusa/src/repositories/idempotency-key.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { IdempotencyKey } from "../models/idempotency-key" + +@EntityRepository(IdempotencyKey) +export class IdempotencyKeyRepository extends Repository {} diff --git a/packages/medusa/src/repositories/line-item.ts b/packages/medusa/src/repositories/line-item.ts new file mode 100644 index 0000000000..01e3e9221e --- /dev/null +++ b/packages/medusa/src/repositories/line-item.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { LineItem } from "../models/line-item" + +@EntityRepository(LineItem) +export class LineItemRepository extends Repository {} diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts new file mode 100644 index 0000000000..e882dab06f --- /dev/null +++ b/packages/medusa/src/repositories/money-amount.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { MoneyAmount } from "../models/money-amount" + +@EntityRepository(MoneyAmount) +export class MoneyAmountRepository extends Repository { } diff --git a/packages/medusa/src/repositories/oauth.ts b/packages/medusa/src/repositories/oauth.ts new file mode 100644 index 0000000000..dd9bc509e5 --- /dev/null +++ b/packages/medusa/src/repositories/oauth.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Oauth } from "../models/oauth" + +@EntityRepository(Oauth) +export class OauthRepository extends Repository {} diff --git a/packages/medusa/src/repositories/order.ts b/packages/medusa/src/repositories/order.ts new file mode 100644 index 0000000000..2ab2992f00 --- /dev/null +++ b/packages/medusa/src/repositories/order.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Order } from "../models/order" + +@EntityRepository(Order) +export class OrderRepository extends Repository {} diff --git a/packages/medusa/src/repositories/payment-provider.ts b/packages/medusa/src/repositories/payment-provider.ts new file mode 100644 index 0000000000..7237b3e48b --- /dev/null +++ b/packages/medusa/src/repositories/payment-provider.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { PaymentProvider } from "../models/payment-provider" + +@EntityRepository(PaymentProvider) +export class PaymentProviderRepository extends Repository {} diff --git a/packages/medusa/src/repositories/payment-session.ts b/packages/medusa/src/repositories/payment-session.ts new file mode 100644 index 0000000000..9e526f4d68 --- /dev/null +++ b/packages/medusa/src/repositories/payment-session.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { PaymentSession } from "../models/payment-session" + +@EntityRepository(PaymentSession) +export class PaymentSessionRepository extends Repository {} diff --git a/packages/medusa/src/repositories/payment.ts b/packages/medusa/src/repositories/payment.ts new file mode 100644 index 0000000000..dc74d3c685 --- /dev/null +++ b/packages/medusa/src/repositories/payment.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Payment } from "../models/payment" + +@EntityRepository(Payment) +export class PaymentRepository extends Repository { } diff --git a/packages/medusa/src/repositories/product-option-value.ts b/packages/medusa/src/repositories/product-option-value.ts new file mode 100644 index 0000000000..8a5cb7e771 --- /dev/null +++ b/packages/medusa/src/repositories/product-option-value.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ProductOptionValue } from "../models/product-option-value" + +@EntityRepository(ProductOptionValue) +export class ProductOptionValueRepository extends Repository { } diff --git a/packages/medusa/src/repositories/product-option.ts b/packages/medusa/src/repositories/product-option.ts new file mode 100644 index 0000000000..1edb3762d9 --- /dev/null +++ b/packages/medusa/src/repositories/product-option.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ProductOption } from "../models/product-option" + +@EntityRepository(ProductOption) +export class ProductOptionRepository extends Repository { } diff --git a/packages/medusa/src/repositories/product-variant.ts b/packages/medusa/src/repositories/product-variant.ts new file mode 100644 index 0000000000..cf1d970606 --- /dev/null +++ b/packages/medusa/src/repositories/product-variant.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ProductVariant } from "../models/product-variant" + +@EntityRepository(ProductVariant) +export class ProductVariantRepository extends Repository {} diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts new file mode 100644 index 0000000000..63659e200f --- /dev/null +++ b/packages/medusa/src/repositories/product.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Product } from "../models/product" + +@EntityRepository(Product) +export class ProductRepository extends Repository {} diff --git a/packages/medusa/src/repositories/refund.ts b/packages/medusa/src/repositories/refund.ts new file mode 100644 index 0000000000..17c5899d9f --- /dev/null +++ b/packages/medusa/src/repositories/refund.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Refund } from "../models/refund" + +@EntityRepository(Refund) +export class RefundRepository extends Repository {} diff --git a/packages/medusa/src/repositories/region.ts b/packages/medusa/src/repositories/region.ts new file mode 100644 index 0000000000..c62da11480 --- /dev/null +++ b/packages/medusa/src/repositories/region.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Region } from "../models/region" + +@EntityRepository(Region) +export class RegionRepository extends Repository {} diff --git a/packages/medusa/src/repositories/return-item.ts b/packages/medusa/src/repositories/return-item.ts new file mode 100644 index 0000000000..0b9937f649 --- /dev/null +++ b/packages/medusa/src/repositories/return-item.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ReturnItem } from "../models/return-item" + +@EntityRepository(ReturnItem) +export class ReturnItemRepository extends Repository {} diff --git a/packages/medusa/src/repositories/return.ts b/packages/medusa/src/repositories/return.ts new file mode 100644 index 0000000000..23e4aa9397 --- /dev/null +++ b/packages/medusa/src/repositories/return.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Return } from "../models/return" + +@EntityRepository(Return) +export class ReturnRepository extends Repository {} diff --git a/packages/medusa/src/repositories/shipping-method.ts b/packages/medusa/src/repositories/shipping-method.ts new file mode 100644 index 0000000000..347026ad9f --- /dev/null +++ b/packages/medusa/src/repositories/shipping-method.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ShippingMethod } from "../models/shipping-method" + +@EntityRepository(ShippingMethod) +export class ShippingMethodRepository extends Repository {} diff --git a/packages/medusa/src/repositories/shipping-option-requirement.ts b/packages/medusa/src/repositories/shipping-option-requirement.ts new file mode 100644 index 0000000000..13c5c9a5f8 --- /dev/null +++ b/packages/medusa/src/repositories/shipping-option-requirement.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ShippingOptionRequirement } from "../models/shipping-option-requirement" + +@EntityRepository(ShippingOptionRequirement) +export class ShippingOptionRequirementRepository extends Repository {} diff --git a/packages/medusa/src/repositories/shipping-option.ts b/packages/medusa/src/repositories/shipping-option.ts new file mode 100644 index 0000000000..eb9a7d9c5d --- /dev/null +++ b/packages/medusa/src/repositories/shipping-option.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ShippingOption } from "../models/shipping-option" + +@EntityRepository(ShippingOption) +export class ShippingOptionRepository extends Repository {} diff --git a/packages/medusa/src/repositories/shipping-profile.ts b/packages/medusa/src/repositories/shipping-profile.ts new file mode 100644 index 0000000000..d9a027f329 --- /dev/null +++ b/packages/medusa/src/repositories/shipping-profile.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { ShippingProfile } from "../models/shipping-profile" + +@EntityRepository(ShippingProfile) +export class ShippingProfileRepository extends Repository {} diff --git a/packages/medusa/src/repositories/staged-job.ts b/packages/medusa/src/repositories/staged-job.ts new file mode 100644 index 0000000000..0323ab29a3 --- /dev/null +++ b/packages/medusa/src/repositories/staged-job.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { StagedJob } from "../models/staged-job" + +@EntityRepository(StagedJob) +export class StagedJobRepository extends Repository {} diff --git a/packages/medusa/src/repositories/store.ts b/packages/medusa/src/repositories/store.ts new file mode 100644 index 0000000000..c3922a1933 --- /dev/null +++ b/packages/medusa/src/repositories/store.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Store } from "../models/store" + +@EntityRepository(Store) +export class StoreRepository extends Repository {} diff --git a/packages/medusa/src/repositories/swap.ts b/packages/medusa/src/repositories/swap.ts new file mode 100644 index 0000000000..015fc56bba --- /dev/null +++ b/packages/medusa/src/repositories/swap.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Swap } from "../models/swap" + +@EntityRepository(Swap) +export class SwapRepository extends Repository {} diff --git a/packages/medusa/src/repositories/user.ts b/packages/medusa/src/repositories/user.ts new file mode 100644 index 0000000000..ead949ad13 --- /dev/null +++ b/packages/medusa/src/repositories/user.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { User } from "../models/user" + +@EntityRepository(User) +export class UserRepository extends Repository {} diff --git a/packages/medusa/src/scripts/mongo-sql-migration.js b/packages/medusa/src/scripts/mongo-sql-migration.js new file mode 100644 index 0000000000..9f7d6f90d0 --- /dev/null +++ b/packages/medusa/src/scripts/mongo-sql-migration.js @@ -0,0 +1,1098 @@ +#!/usr/bin/env node + +import path from "path" +import glob from "glob" +import mongo from "mongodb" +import chalk from "chalk" +import { QueryRunner, In, createConnection } from "typeorm" + +import { getConfigFile, createRequireFromPath } from "medusa-core-utils" + +import { MoneyAmount } from "../models/money-amount" +import { Country } from "../models/country" +import { Currency } from "../models/currency" +import { Discount } from "../models/discount" +import { Customer } from "../models/customer" +import { Order } from "../models/order" +import { LineItem } from "../models/line-item" +import { Fulfillment } from "../models/fulfillment" +import { FulfillmentItem } from "../models/fulfillment-item" +import { ReturnItem } from "../models/return-item" +import { FulfillmentProvider } from "../models/fulfillment-provider" +import { PaymentProvider } from "../models/payment-provider" +import { Payment } from "../models/payment" +import { Swap } from "../models/swap" +import { GiftCard } from "../models/gift-card" +import { Region } from "../models/region" +import { Refund } from "../models/refund" +import { Return } from "../models/return" +import { Address } from "../models/address" +import { ProductVariant } from "../models/product-variant" +import { ShippingMethod } from "../models/shipping-method" +import { ShippingOption } from "../models/shipping-option" +import { ShippingProfile } from "../models/shipping-profile" +import { DiscountRule } from "../models/discount-rule" +import { Store } from "../models/store" +import { ProductOption } from "../models/product-option" +import { ProductOptionValue } from "../models/product-option-value" +import { ShippingOptionRequirement } from "../models/shipping-option-requirement" + +import { RegionRepository } from "../repositories/region" +import { DiscountRepository } from "../repositories/discount" +import { GiftCardRepository } from "../repositories/gift-card" +import { ShippingProfileRepository } from "../repositories/shipping-profile" +import { ShippingOptionRepository } from "../repositories/shipping-option" +import { ProductRepository } from "../repositories/product" +import { ProductVariantRepository } from "../repositories/product-variant" + +/** + * Migrate store + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateStore = async (mongodb, queryRunner) => { + const dcol = mongodb.collection("stores") + + const dcur = dcol.find({}) + const stores = await dcur.toArray() + + const storeRepo = queryRunner.manager.getRepository(Store) + const currencyRepo = queryRunner.manager.getRepository(Currency) + + for (const d of stores) { + const newly = storeRepo.create({ + name: d.name, + default_currency_code: d.default_currency.toLowerCase(), + currencies: await Promise.all( + d.currencies.map(c => currencyRepo.findOne({ code: c.toLowerCase() })) + ), + swap_link_template: d.swap_link_template, + }) + await storeRepo.save(newly) + } +} + +/** + * Migrates Regions + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateRegions = async (mongodb, queryRunner) => { + const rcol = mongodb.collection("regions") + const regCursor = rcol.find({}) + const regions = await regCursor.toArray() + + const countryRepository = queryRunner.manager.getRepository(Country) + const payRepository = queryRunner.manager.getRepository(PaymentProvider) + const fulRepository = queryRunner.manager.getRepository(FulfillmentProvider) + + const regionRepository = queryRunner.manager.getCustomRepository( + RegionRepository + ) + + for (const reg of regions) { + const countries = await countryRepository.find({ + iso_2: In(reg.countries.map(c => c.toLowerCase())), + }) + + const newRegion = regionRepository.create({ + id: `${reg._id}`, + name: reg.name, + currency_code: reg.currency_code.toLowerCase(), + tax_rate: reg.tax_rate * 100, + tax_code: reg.tax_code, + countries, + }) + + newRegion.payment_providers = [] + for (const pp of reg.payment_providers) { + let exists = await payRepository.findOne({ id: pp }) + if (!exists) { + let newly = payRepository.create({ + id: pp, + is_installed: false, + }) + exists = await payRepository.save(newly) + } + + newRegion.payment_providers.push(exists) + } + + newRegion.fulfillment_providers = [] + for (const pp of reg.fulfillment_providers) { + let exists = await fulRepository.findOne({ id: pp }) + if (!exists) { + let newly = fulRepository.create({ + id: pp, + is_installed: false, + }) + exists = await fulRepository.save(newly) + } + + newRegion.fulfillment_providers.push(exists) + } + + await regionRepository.save(newRegion) + } +} + +/** + * Migrates Shipping Options + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateShippingOptions = async (mongodb, queryRunner) => { + const col = mongodb.collection("shippingoptions") + const cursor = col.find({}) + const options = await cursor.toArray() + + // const rCol = mongodb.collection("regions") + // const rCursor = rCol.find({}) + // const regions = await rCursor.toArray() + + const pCol = mongodb.collection("shippingprofiles") + const pCursor = pCol.find({}) + const profiles = await pCursor.toArray() + + const reqRepo = queryRunner.manager.getRepository(ShippingOptionRequirement) + //const regionRepository = queryRunner.manager.getCustomRepository( + // RegionRepository + //) + const optionRepository = queryRunner.manager.getCustomRepository( + ShippingOptionRepository + ) + const profileRepo = queryRunner.manager.getCustomRepository( + ShippingProfileRepository + ) + + for (const option of options) { + // const mongoReg = regions.find(r => r._id.equals(option.region_id)) + // const region = await regionRepository.findOne({ name: mongoReg.name }) + + const mongoProfile = profiles.find(p => p._id.equals(option.profile_id)) + let profile + if (mongoProfile.name === "default_shipping_profile") { + profile = await profileRepo.findOne({ type: "default" }) + } else if ((mongoProfile.name = "default_gift_card_profile")) { + profile = await profileRepo.findOne({ type: "gift_card" }) + } + + const newOption = optionRepository.create({ + id: `${option._id}`, + name: option.name, + region_id: option.region_id, + profile, + provider_id: option.provider_id, + price_type: option.price.type, + amount: Math.round(option.price.amount * 100), + is_return: !!option.is_return, + data: option.data, + requirements: option.requirements.map(r => + reqRepo.create({ + type: r.type, + amount: Math.round(r.value * 100), + }) + ), + }) + await optionRepository.save(newOption) + } +} + +/** + * Migrates products and product variants + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateProducts = async (mongodb, queryRunner) => { + const col = mongodb.collection("products") + const cursor = col.find({}) + const products = await cursor.toArray() + + const variantCol = mongodb.collection("productvariants") + + const maRepo = queryRunner.manager.getRepository(MoneyAmount) + const optValRepo = queryRunner.manager.getRepository(ProductOptionValue) + const optRepo = queryRunner.manager.getRepository(ProductOption) + const varRepo = queryRunner.manager.getCustomRepository( + ProductVariantRepository + ) + const prodRepo = queryRunner.manager.getCustomRepository(ProductRepository) + const profileRepo = queryRunner.manager.getCustomRepository( + ShippingProfileRepository + ) + + const defProf = await profileRepo.findOne({ type: "default" }) + const gcProf = await profileRepo.findOne({ type: "gift_card" }) + + for (const p of products) { + const newOptions = await Promise.all( + p.options.map(o => { + const newO = optRepo.create({ + id: `${o._id}`, + title: o.title, + }) + return optRepo.save(newO) + }) + ) + + const varCur = variantCol.find({ + _id: { $in: p.variants.map(id => new mongo.ObjectID(id)) }, + }) + const mongoVariants = await varCur.toArray() + + const newVariants = await Promise.all( + mongoVariants.map(v => { + const newV = varRepo.create({ + id: `${v._id}`, + title: v.title, + barcode: v.ean, + ean: v.ean, + sku: v.sku, + manage_inventory: v.manage_inventory, + allow_backorder: v.allow_backorder, + inventory_quantity: v.inventory_quantity, + options: v.options.map((o, idx) => { + const opt = newOptions[idx] + return optValRepo.create({ + value: o.value, + option: opt, + }) + }), + prices: v.prices.map(p => + maRepo.create({ + currency_code: p.currency_code.toLowerCase(), + amount: Math.round(p.amount * 100), + }) + ), + hs_code: v.metadata && v.metadata.hs_code, + origin_country: v.metadata && v.metadata.origin_country, + metadata: v.metadata && { + alternative_size: v.metadata.alternative_size, + color: v.metadata.color, + }, + }) + return newV + }) + ) + + const newProd = prodRepo.create({ + id: `${p._id}`, + title: p.title, + tags: p.tags || null, + description: p.description, + handle: p.handle, + is_giftcard: p.is_giftcard, + thumbnail: p.thumbnail, + profile: p.is_giftcard ? gcProf : defProf, + options: newOptions, + variants: newVariants, + }) + await prodRepo.save(newProd) + } +} + +const createDiscount = async (mongodb, queryRunner, d) => { + const rcol = mongodb.collection("regions") + + const ruleRepo = queryRunner.manager.getRepository(DiscountRule) + const gcRepo = queryRunner.manager.getCustomRepository(GiftCardRepository) + const discountRepo = queryRunner.manager.getCustomRepository( + DiscountRepository + ) + const regRepo = queryRunner.manager.getCustomRepository(RegionRepository) + + if (d.is_giftcard) { + const rcur = rcol.find({ + _id: mongo.ObjectID(d.regions[0]), + }) + const mongoRegs = await rcur.toArray() + const region = await regRepo.findOne({ name: mongoRegs[0].name }) + + const newD = gcRepo.create({ + id: `${d._id}`, + code: d.code, + is_disabled: d.disabled, + value: !!d.original_amount ? Math.round(d.original_amount * 100) : 0, + balance: Math.round(d.discount_rule.value * 100), + region, + }) + + return gcRepo.save(newD) + } else { + const rcur = rcol.find({ + _id: { $in: d.regions.map(id => mongo.ObjectID(id)) }, + }) + const mongoRegs = await rcur.toArray() + const regions = await regRepo.find({ + id: In(mongoRegs.map(r => `${r._id}`)), + }) + const newD = discountRepo.create({ + id: `${d._id}`, + code: d.code, + is_dynamic: !!d.is_dynamic, + rule: ruleRepo.create({ + description: d.discount_rule.description, + type: d.discount_rule.type, + allocation: d.discount_rule.allocation, + value: + d.discount_rule.type === "percentage" + ? d.discount_rule.value + : Math.round(d.discount_rule.value * 100), + usage_limit: d.discount_rule.usage_limit, + }), + is_disabled: d.disabled, + regions, + }) + + return discountRepo.save(newD) + } +} + +/** + * Migrates discounts + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateDiscounts = async (mongodb, queryRunner) => { + const dcol = mongodb.collection("discounts") + + const dcur = dcol.find({}) + const discounts = await dcur.toArray() + for (const d of discounts) { + await createDiscount(mongodb, queryRunner, d) + } + + await migrateDynamicDiscounts(mongodb, queryRunner) +} + +/** + * Migrates dynamic discounts + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateDynamicDiscounts = async (mongodb, queryRunner) => { + const dcol = mongodb.collection("discounts") + const dyncol = mongodb.collection("dynamicdiscountcodes") + + const dcur = dyncol.find({}) + const dynamicCodes = await dcur.toArray() + + const discountRepo = queryRunner.manager.getCustomRepository( + DiscountRepository + ) + + const mongoV = {} + const visited = {} + + const toSave = [] + for (const d of dynamicCodes) { + let disc + if (mongoV[d.discount_id]) { + disc = mongoV[d.discount_id] + } else { + disc = await dcol.findOne({ _id: mongo.ObjectID(d.discount_id) }) + mongoV[d.discount_id] = disc + } + + let discount + if (visited[disc.code]) { + discount = visited[disc.code] + } else { + const pare = await discountRepo.findOne({ code: disc.code }) + discount = pare + visited[disc.code] = pare + } + + const newD = discountRepo.create({ + code: d.code, + is_dynamic: true, + is_disabled: d.disabled, + parent_discount: discount, + rule_id: discount.rule_id, + }) + + toSave.push(newD) + } + + return discountRepo.save(toSave) +} + +/** + * Migrates customers + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateCustomers = async (mongodb, queryRunner) => { + const col = mongodb.collection("customers") + + const cur = col.find({}) + const customers = await cur.toArray() + + const customerRepo = queryRunner.manager.getRepository(Customer) + + const toSave = [] + for (const c of customers) { + if (toSave.find(s => s.email === c.email.toLowerCase())) { + continue + } + + const newC = customerRepo.create({ + id: `${c._id}`, + email: c.email.toLowerCase(), + first_name: c.first_name, + last_name: c.last_name, + phone: c.phone, + has_account: c.has_account, + password_hash: c.password_hash, + metadata: c.metadata, + }) + toSave.push(newC) + } + return customerRepo.save(toSave, { chunk: 1000 }) +} + +/** + * Migrates orders + * @param {MongoDb} mongodb + * @param {QueryRunner} queryRunner + */ +const migrateOrders = async (mongodb, queryRunner) => { + const swapCol = mongodb.collection("swaps") + + const col = mongodb.collection("orders") + + const cur = col.find({}) + const orders = await cur.toArray() + + const customerRepo = queryRunner.manager.getRepository(Customer) + const orderRepo = queryRunner.manager.getRepository(Order) + const lineItemRepo = queryRunner.manager.getRepository(LineItem) + const fulItemRepo = queryRunner.manager.getRepository(FulfillmentItem) + const retItemRepo = queryRunner.manager.getRepository(ReturnItem) + const fulfillmentRepo = queryRunner.manager.getRepository(Fulfillment) + const paymentRepo = queryRunner.manager.getRepository(Payment) + const refundRepo = queryRunner.manager.getRepository(Refund) + const returnRepo = queryRunner.manager.getRepository(Return) + const gcRepo = queryRunner.manager.getRepository(GiftCard) + const swapRepo = queryRunner.manager.getRepository(Swap) + const discountRepo = queryRunner.manager.getRepository(Discount) + const methodRepo = queryRunner.manager.getRepository(ShippingMethod) + const optionRepo = queryRunner.manager.getRepository(ShippingOption) + const addressRepo = queryRunner.manager.getRepository(Address) + const profileRepo = queryRunner.manager.getRepository(ShippingProfile) + const fulProvRepo = queryRunner.manager.getRepository(FulfillmentProvider) + + const paymentsToSave = [] + const refundsToSave = [] + const returnsToSave = [] + const swapsToSave = [] + const shippingMethodsToSave = [] + const lineItemsToSave = [] + const ordersToSave = [] + const giftCardsToSave = [] + const discountsToSave = [] + const fulfillToSave = [] + + for (const o of orders) { + // const mongoreg = regions.find(r => r._id.equals(o.region_id)) + // const region = await regionRepo.findOne({ name: mongoreg.name }) + + /************************************************************************* + * SHIPPING METHODS + *************************************************************************/ + const createShippingMethod = async m => { + let shippingOption = await optionRepo.findOne({ + id: `${m._id}`, + }) + if (!shippingOption) { + const profile = await profileRepo.findOne({ type: "default" }) + let provider = await fulProvRepo.findOne({ id: m.provider_id }) + if (!provider) { + const newly = fulProvRepo.create({ + id: m.provider_id, + is_installed: false, + }) + provider = await fulProvRepo.save(newly) + } + const newly = optionRepo.create({ + name: m.name, + region_id: o.region_id, + profile_id: profile.id, + price_type: "flat_rate", + amount: Math.round(m.price * 100), + data: m.data, + is_return: false, + deleted_at: new Date(), + provider, + }) + shippingOption = await optionRepo.save(newly) + } + + return methodRepo.create({ + order_id: `${o._id}`, + shipping_option_id: shippingOption.id, + price: Math.round(m.price * 100), + data: m.data, + }) + } + + for (const m of o.shipping_methods) { + const method = await createShippingMethod(m) + shippingMethodsToSave.push(method) + } + + /************************************************************************* + * CUSTOMER + *************************************************************************/ + let customer = await customerRepo.findOne({ email: o.email.toLowerCase() }) + if (!customer) { + const n = customerRepo.create({ + email: o.email.toLowerCase(), + }) + customer = await customerRepo.save(n) + } + + /************************************************************************* + * LINE ITEMS + *************************************************************************/ + const createLineItem = (li, custom = {}) => { + let fulfilled_quantity = Math.min(li.fulfilled_quantity || 0, li.quantity) + let shipped_quantity = Math.min( + li.fulfilled_quantity || 0, + li.shipped_quantity || 0 + ) + let returned_quantity = Math.min( + li.shipped_quantity || 0, + li.returned_quantity || 0 + ) + + return lineItemRepo.create({ + ...custom, + id: `${li._id}`, + title: li.title, + description: li.description, + quantity: li.quantity, + is_giftcard: !!li.is_giftcard, + should_merge: !!li.should_merge, + allow_discounts: !li.no_discount, + thumbnail: li.thumbnail, + unit_price: Math.round(li.content.unit_price * 100), + variant_id: li.content.variant._id ? `${li.content.variant._id}` : null, + fulfilled_quantity, + shipped_quantity, + returned_quantity, + metadata: li.metadata, + }) + } + + for (const li of o.items) { + const lineitem = createLineItem(li, { + order_id: `${o._id}`, + }) + lineItemsToSave.push(lineitem) + } + + /************************************************************************* + * DISCOUNT + *************************************************************************/ + const giftCards = [] + const discounts = [] + for (const d of o.discounts) { + if (d.is_giftcard) { + let gc = await gcRepo.findOne({ code: d.code }) + if (!gc) { + gc = await createDiscount(mongodb, queryRunner, d) + } + giftCards.push(gc) + } else { + let disc = await discountRepo.findOne({ code: d.code }) + if (!disc) { + disc = await createDiscount(mongodb, queryRunner, d) + } + discounts.push(disc) + } + } + + /************************************************************************* + * ADDREESS + *************************************************************************/ + const address = addressRepo.create({ + customer, + first_name: o.shipping_address.first_name, + last_name: o.shipping_address.last_name, + address_1: o.shipping_address.address_1, + address_2: o.shipping_address.address_2, + city: o.shipping_address.city, + country_code: o.shipping_address.country_code.toLowerCase(), + province: o.shipping_address.province, + postal_code: o.shipping_address.postal_code, + phone: o.shipping_address.phone, + }) + + /************************************************************************* + * CREATE ORDER + *************************************************************************/ + const nOrder = orderRepo.create({ + id: `${o._id}`, + display_id: o.display_id, + tax_rate: o.tax_rate * 100, + currency_code: o.currency_code.toLowerCase(), + email: o.email.toLowerCase(), + status: o.status, + fulfillment_status: o.fulfillment_status, + payment_status: o.payment_status, + shipping_address: address, + billing_address: address, + // shipping_methods: shippingMethods, + // items: lineItems, + gift_cards: giftCards, + region_id: `${o.region_id}`, + customer, + discounts, + created_at: new Date(parseInt(o.created)), + canceled_at: o.status === "canceled" ? new Date() : null, + }) + + ordersToSave.push(nOrder) + //let or = await orderRepo.save(nOrder) + //or.display_id = o.display_id + + /************************************************************************* + * FULFILLMENTS + *************************************************************************/ + const createFulfillment = (f, custom = {}) => { + if (!f || !f._id) { + console.log("found empty") + } + + const items = f.items.map(fi => { + return fulItemRepo.create({ + item_id: `${fi._id}`, + quantity: fi.quantity, + }) + }) + + const toCreate = { + id: `${f._id}`, + ...custom, + items, + provider_id: f.provider_id, + tracking_numbers: f.tracking_numbers, + data: {}, + metadata: f.metadata, + canceled_at: f.is_canceled ? new Date() : null, + shipped_at: f.shipped_at ? new Date(parseInt(f.shipped_at)) : null, + } + + if (!!f.created) { + toCreate.created_at = new Date(parseInt(f.created)) + } + + return fulfillmentRepo.create(toCreate) + } + + for (const f of o.fulfillments) { + if (!f || !f._id) { + continue + } + const ful = createFulfillment(f, { order_id: `${o._id}` }) + fulfillToSave.push(ful) + } + + /************************************************************************* + * REFUNDS + *************************************************************************/ + const refunds = [] + const totalRefund = 0 + for (const r of o.refunds) { + const reason = r.reason || "return" + totalRefund += r.amount + refundsToSave.push( + refundRepo.create({ + order_id: `${o._id}`, + currency_code: o.currency_code.toLowerCase(), + amount: Math.round(r.amount * 100), + reason, + note: r.note, + created_at: new Date(parseInt(r.created)), + }) + ) + } + // or.refunds = refunds + + const createReturn = async (r, custom = {}) => { + const m = r.shipping_method + let method + if (m && m.name) { + let shippingOption = await optionRepo.findOne({ + name: m.name, + region_id: o.region_id, + }) + if (!shippingOption) { + const profile = await profileRepo.findOne({ type: "default" }) + let provider = await fulProvRepo.findOne({ id: m.provider_id }) + if (!provider) { + const newly = fulProvRepo.create({ + id: m.provider_id, + is_installed: false, + }) + provider = await fulProvRepo.save(newly) + } + const newly = optionRepo.create({ + name: m.name, + region_id: o.region_id, + profile_id: profile.id, + price_type: "flat_rate", + amount: Math.round(m.price * 100), + data: m.data, + is_return: true, + deleted_at: new Date(), + provider, + }) + shippingOption = await optionRepo.save(newly) + } + + method = methodRepo.create({ + shipping_option_id: shippingOption.id, + price: Math.round(m.price * 100), + data: m.data, + }) + } + + const items = r.items.map(raw => { + //const ri = o.items.find(i => i._id.equals(raw.item_id)) + //const original = or.items.find( + // li => li.title === ri.title && li.description === ri.description + //) + + return retItemRepo.create({ + item_id: raw.item_id, + quantity: raw.quantity, + requested_quantity: raw.is_requested ? raw.quantity : null, + received_quantity: raw.is_registered ? raw.quantity : null, + }) + }) + + return returnRepo.create({ + id: `${r._id}`, + status: r.status || "received", + ...custom, + refund_amount: Math.round(r.refund_amount * 100), + shipping_method: method, + shipping_data: r.shipping_data, + items, + received_at: r.status === "received" ? new Date() : null, + created_at: new Date(parseInt(r.created)), + metadata: r.metadata, + }) + } + + /************************************************************************* + * RETURNS + *************************************************************************/ + for (const r of o.returns) { + if (r.items.length === 0) { + continue + } + + const ret = await createReturn(r, { order_id: `${o._id}` }) + returnsToSave.push(ret) + } + + // or.returns = returns + + /************************************************************************* + * SWAPS + *************************************************************************/ + if (o.swaps) { + const swapCur = swapCol.find({ + _id: { $in: o.swaps.map(i => mongo.ObjectID(i)) }, + }) + const oSwaps = await swapCur.toArray() + if (oSwaps.length) { + // let swaps = [] + for (const s of oSwaps) { + if (!s.return) continue + + for (const li of s.additional_items) { + lineItemsToSave.push(createLineItem(li, { swap_id: `${s._id}` })) + } + + const toCreate = { + id: `${s._id}`, + order_id: `${o._id}`, + fulfillment_status: + s.fulfillment_status === "shipped" ? "shipped" : "not_fulfilled", + payment_status: s.payment_status, + shipping_methods: await Promise.all( + s.shipping_methods.map(createShippingMethod) + ), + created_at: new Date(parseInt(s.created)), + } + + if (s.shipping_address) { + const address = addressRepo.create({ + customer, + first_name: s.shipping_address.first_name, + last_name: s.shipping_address.last_name, + address_1: s.shipping_address.address_1, + address_2: s.shipping_address.address_2, + city: s.shipping_address.city, + country_code: s.shipping_address.country_code.toLowerCase(), + province: s.shipping_address.province, + postal_code: s.shipping_address.postal_code, + phone: s.shipping_address.phone, + }) + toCreate.shipping_address = address + } + + if (s.return) { + returnsToSave.push( + await createReturn(s.return, { + swap_id: `${s._id}`, + }) + ) + } + + if (s.payment_method) { + toCreate.payment = paymentRepo.create({ + amount: + (s.payment_method.data && s.payment_method.data.amount) || 0, + currency_code: s.currency_code.toLowerCase(), + amount_refunded: 0, + provider_id: o.payment_method.provider_id, + data: o.payment_method.data, + canceled_at: o.payment_status === "canceled" ? new Date() : null, + captured_at: + o.payment_status === "captured" || + o.payment_status === "refunded" || + o.payment_status === "partially" + ? new Date() + : null, + }) + } + + if ((s.fulfillments && s.fulfillments.length) > 0) { + for (const f of s.fulfillments) { + if (!f || !f._id) { + continue + } + fulfillToSave.push(createFulfillment(f, { swap_id: `${s._id}` })) + } + } + + const newly = swapRepo.create(toCreate) + swapsToSave.push(newly) + } + + // or.swaps = swaps + } + } + + /************************************************************************* + * PAYMENTS + *************************************************************************/ + const amount = + o.payment_method.provider_id === "stripe" + ? o.payment_method.data.amount + : o.payment_method.data.order_amount || + (o.payment_method.data.amount && + o.payment_method.data.amount.value) || + 0 + paymentsToSave.push( + paymentRepo.create({ + order_id: `${o._id}`, + amount, + currency_code: o.currency_code.toLowerCase(), + amount_refunded: Math.round(totalRefund * 100), + provider_id: o.payment_method.provider_id, + data: o.payment_method.data, + canceled_at: o.payment_status === "canceled" ? new Date() : null, + captured_at: + o.payment_status === "captured" || + o.payment_status === "refunded" || + o.payment_status === "partially_refunded" + ? new Date() + : null, + }) + ) + + // await orderRepo.save(or) + + if (o.display_id % 100 === 0) { + console.log(o.display_id) + } + } + + const newOs = await orderRepo.save(ordersToSave, { chunk: 1000 }) + await swapRepo.save(swapsToSave, { chunk: 1000 }) + await lineItemRepo.save(lineItemsToSave, { chunk: 1000 }) + await methodRepo.save(shippingMethodsToSave, { chunk: 1000 }) + await refundRepo.save(refundsToSave, { chunk: 1000 }) + await returnRepo.save(returnsToSave, { chunk: 1000 }) + await gcRepo.save(giftCardsToSave, { chunk: 1000 }) + console.log("done with gcs") + await discountRepo.save(discountsToSave, { chunk: 1000 }) + console.log("done with discounts") + await fulfillmentRepo.save(fulfillToSave, { chunk: 1000 }) + + for (const o of orders) { + await queryRunner.query(`UPDATE "order" SET display_id=$1 WHERE id=$2`, [ + o.display_id, + `${o._id}`, + ]) + } + + const last = orders[orders.length - 1] + await queryRunner.query( + `ALTER SEQUENCE order_display_id_seq RESTART WITH ${parseInt( + last.display_id + ) + 1}` + ) +} + +const migrate = async () => { + const root = path.resolve(".") + const { configModule } = getConfigFile(root, "medusa-config") + const { + mongo_url, + database_type, + database_url, + database_extra, + } = configModule.projectConfig + + if (!mongo_url) { + throw new Error( + "Cannot run migration script without a mongo_url in medusa-config" + ) + } + + if (!database_type || !database_url) { + throw new Error( + "Cannot run migration script without a database_type and database_url in medusa-config" + ) + } + + const mPath = path.resolve(__dirname, "../models") + + console.log(chalk.blue("MONGO:"), "Connecting to ", mongo_url) + const client = await mongo.MongoClient.connect(mongo_url, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + + const db = client.db(client.dbName) + console.log(chalk.green("MONGO:"), "Connecting created") + + console.log(chalk.blue("SQL:"), "Connecting to ", database_url) + const sqlConnection = await createConnection({ + type: database_type, + url: database_url, + extra: database_extra || {}, + entities: [`${mPath}/*.js`], + // logging: true, + }) + const queryRunner = sqlConnection.createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction() + console.log(chalk.green("SQL:"), "Connecting created") + + let error + try { + await queryRunner.query( + "UPDATE country SET region_id=NULL WHERE iso_2 IS NOT NULL" + ) + await queryRunner.query(`DELETE FROM store WHERE id IS NOT NULL`) + await queryRunner.query( + `DELETE FROM return_item WHERE return_id IS NOT NULL` + ) + await queryRunner.query( + `DELETE FROM fulfillment_item WHERE fulfillment_id IS NOT NULL` + ) + await queryRunner.query(`DELETE FROM line_item WHERE id IS NOT NULL`) + await queryRunner.query("DELETE FROM gift_card WHERE code IS NOT NULL") + await queryRunner.query("DELETE FROM discount WHERE code IS NOT NULL") + await queryRunner.query("DELETE FROM discount_rule WHERE type IS NOT NULL") + await queryRunner.query( + "DELETE FROM money_amount WHERE currency_code IS NOT NULL" + ) + await queryRunner.query( + `DELETE FROM product_option_value WHERE value IS NOT NULL` + ) + await queryRunner.query( + `DELETE FROM product_option WHERE title IS NOT NULL` + ) + await queryRunner.query( + `DELETE FROM product_variant WHERE title IS NOT NULL` + ) + await queryRunner.query(`DELETE FROM product WHERE title is NOT NULL`) + await queryRunner.query( + `DELETE FROM shipping_option_requirement WHERE id IS NOT NULL` + ) + await queryRunner.query(`DELETE FROM shipping_method WHERE id IS NOT NULL`) + await queryRunner.query( + `DELETE FROM shipping_option WHERE name IS NOT NULL` + ) + await queryRunner.query( + `DELETE FROM order_discounts WHERE order_id IS NOT NULL` + ) + await queryRunner.query(`DELETE FROM payment WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM fulfillment WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM return WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM swap WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM refund WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM "order" WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM address WHERE id IS NOT NULL`) + await queryRunner.query(`DELETE FROM region WHERE name IS NOT NULL`) + await queryRunner.query(`DELETE FROM customer WHERE email IS NOT NULL`) + + await migrateStore(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Store migrated") + }) + + await migrateRegions(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Regions migrated") + }) + + await migrateShippingOptions(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Shipping Options Migrated") + }) + + await migrateProducts(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Products migrated") + }) + + await migrateDiscounts(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Discounts migrated") + }) + + await migrateCustomers(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Customers migrated") + }) + + await migrateOrders(db, queryRunner).then(() => { + console.log(chalk.green("SUCCESS: "), "Orders migrated") + }) + + await queryRunner.commitTransaction() + } catch (err) { + await queryRunner.rollbackTransaction() + error = err + } finally { + await queryRunner.release() + } + + if (error) { + throw error + } +} + +migrate() + .then(() => { + console.log("Migration complete") + process.exit() + }) + .catch(err => { + console.log(err) + process.exit(1) + }) diff --git a/packages/medusa/src/services/__mocks__/cart.js b/packages/medusa/src/services/__mocks__/cart.js index 3844fa5d45..885369456c 100644 --- a/packages/medusa/src/services/__mocks__/cart.js +++ b/packages/medusa/src/services/__mocks__/cart.js @@ -3,12 +3,12 @@ import { IdMap } from "medusa-test-utils" export const carts = { emptyCart: { - _id: IdMap.getId("emptyCart"), + id: IdMap.getId("emptyCart"), items: [], region_id: IdMap.getId("testRegion"), shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", data: { some_data: "yes", @@ -16,72 +16,68 @@ export const carts = { }, ], }, + testCart: { + id: IdMap.getId("test-cart"), + items: [], + payment: { + data: "some-data", + }, + payment_session: { + status: "authorized", + }, + total: 1000, + region_id: IdMap.getId("testRegion"), + }, + testSwapCart: { + id: IdMap.getId("test-swap"), + items: [], + type: "swap", + payment: { + data: "some-data", + }, + payment_session: { + status: "authorized", + }, + metadata: { + swap_id: "test-swap", + }, + total: 1000, + region_id: IdMap.getId("testRegion"), + }, regionCart: { - _id: IdMap.getId("regionCart"), + id: IdMap.getId("regionCart"), name: "Product 1", region_id: IdMap.getId("testRegion"), }, frCart: { - _id: IdMap.getId("fr-cart"), + id: IdMap.getId("fr-cart"), title: "test", region_id: IdMap.getId("region-france"), items: [ { - _id: IdMap.getId("line"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: [ - { - unit_price: 8, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - ], - quantity: 10, - }, - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + unit_price: 10, + variant: { + id: IdMap.getId("eur-10-us-12"), + }, + product: { + id: IdMap.getId("product"), }, quantity: 10, }, ], shipping_methods: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", }, ], @@ -91,21 +87,21 @@ export const carts = { customer_id: "", }, cartWithPaySessions: { - _id: IdMap.getId("cartWithPaySessions"), + id: IdMap.getId("cartWithPaySessions"), region_id: IdMap.getId("testRegion"), items: [ { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 123, variant: { - _id: IdMap.getId("can-cover"), + id: IdMap.getId("can-cover"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -132,12 +128,12 @@ export const carts = { customer_id: "", }, discountCart: { - _id: IdMap.getId("discount-cart"), + id: IdMap.getId("discount-cart"), discounts: [], region_id: IdMap.getId("region-france"), items: [ { - _id: IdMap.getId("line"), + id: IdMap.getId("line"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", @@ -145,20 +141,20 @@ export const carts = { { unit_price: 8, variant: { - _id: IdMap.getId("eur-8-us-10"), + id: IdMap.getId("eur-8-us-10"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, { unit_price: 10, variant: { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -166,17 +162,17 @@ export const carts = { quantity: 10, }, { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 10, variant: { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), }, product: { - _id: IdMap.getId("product"), + id: IdMap.getId("product"), }, quantity: 1, }, @@ -185,24 +181,21 @@ export const carts = { ], }, cartWithMetadataLineItem: { - _id: IdMap.getId("cartLineItemMetadata"), + id: IdMap.getId("cartLineItemMetadata"), discounts: [], region_id: IdMap.getId("region-france"), items: [ { - _id: IdMap.getId("lineWithMetadata"), + id: IdMap.getId("lineWithMetadata"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + unit_price: 10, + variant: { + id: IdMap.getId("eur-10-us-12"), + }, + product: { + id: IdMap.getId("product"), }, quantity: 10, metadata: { @@ -214,6 +207,28 @@ export const carts = { } export const CartServiceMock = { + withTransaction: function() { + return this + }, + updatePaymentSession: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), + authorizePayment: jest.fn().mockImplementation((id, data) => { + if (id === IdMap.getId("test-cart2")) { + return Promise.resolve({ + ...carts.testCart, + payment_session: { status: "requires_more" }, + id: IdMap.getId("test-cart2"), + }) + } + return Promise.resolve(carts.testCart) + }), + refreshPaymentSession: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), + update: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), create: jest.fn().mockImplementation(data => { if (data.region_id === IdMap.getId("testRegion")) { return Promise.resolve(carts.regionCart) @@ -221,11 +236,18 @@ export const CartServiceMock = { if (data.region_id === IdMap.getId("fail")) { throw new MedusaError(MedusaError.Types.INVALID_DATA, "Region not found") } + return Promise.resolve(carts.regionCart) }), retrieve: jest.fn().mockImplementation(cartId => { if (cartId === IdMap.getId("fr-cart")) { return Promise.resolve(carts.frCart) } + if (cartId === IdMap.getId("swap-cart")) { + return Promise.resolve(carts.testSwapCart) + } + if (cartId === IdMap.getId("test-cart")) { + return Promise.resolve(carts.testCart) + } if (cartId === IdMap.getId("cartLineItemMetadata")) { return Promise.resolve(carts.cartWithMetadataLineItem) } @@ -317,13 +339,13 @@ export const CartServiceMock = { retrieveShippingOption: jest.fn().mockImplementation((cartId, optionId) => { if (optionId === IdMap.getId("freeShipping")) { return { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), profile_id: "default_profile", } } if (optionId === IdMap.getId("withData")) { return { - _id: IdMap.getId("withData"), + id: IdMap.getId("withData"), profile_id: "default_profile", data: { some_data: "yes", diff --git a/packages/medusa/src/services/__mocks__/counter.js b/packages/medusa/src/services/__mocks__/counter.js deleted file mode 100644 index 7b6fc34faa..0000000000 --- a/packages/medusa/src/services/__mocks__/counter.js +++ /dev/null @@ -1,11 +0,0 @@ -export const CounterServiceMock = { - getNext: jest.fn().mockImplementation(data => { - return Promise.resolve() - }), -} - -const mock = jest.fn().mockImplementation(() => { - return CounterServiceMock -}) - -export default mock diff --git a/packages/medusa/src/services/__mocks__/customer.js b/packages/medusa/src/services/__mocks__/customer.js index fd9294ce6c..78e65409e4 100644 --- a/packages/medusa/src/services/__mocks__/customer.js +++ b/packages/medusa/src/services/__mocks__/customer.js @@ -3,10 +3,10 @@ import { IdMap } from "medusa-test-utils" export const CustomerServiceMock = { create: jest.fn().mockImplementation(data => { - return Promise.resolve(data) + return Promise.resolve({ ...data, id: IdMap.getId("lebron") }) }), update: jest.fn().mockImplementation((id, data) => { - return Promise.resolve(data) + return Promise.resolve({ ...data, id: IdMap.getId("lebron") }) }), decorate: jest.fn().mockImplementation(data => { let d = Object.assign({}, data) @@ -19,7 +19,7 @@ export const CustomerServiceMock = { retrieve: jest.fn().mockImplementation(id => { if (id === IdMap.getId("lebron")) { return Promise.resolve({ - _id: IdMap.getId("lebron"), + id: IdMap.getId("lebron"), first_name: "LeBron", last_name: "James", email: "lebron@james.com", @@ -30,14 +30,14 @@ export const CustomerServiceMock = { retrieveByEmail: jest.fn().mockImplementation(email => { if (email === "lebron@james.com") { return Promise.resolve({ - _id: IdMap.getId("lebron"), + id: IdMap.getId("lebron"), email, password_hash: "1234", }) } if (email === "test@testdom.com") { return Promise.resolve({ - _id: IdMap.getId("testdom"), + id: IdMap.getId("testdom"), email, password_hash: "1234", }) diff --git a/packages/medusa/src/services/__mocks__/discount.js b/packages/medusa/src/services/__mocks__/discount.js index 63086b6a66..c880b26a9e 100644 --- a/packages/medusa/src/services/__mocks__/discount.js +++ b/packages/medusa/src/services/__mocks__/discount.js @@ -1,7 +1,130 @@ import { IdMap } from "medusa-test-utils" -import { discounts } from "../../models/__mocks__/discount" + +export const discounts = { + dynamic: { + id: IdMap.getId("dynamic"), + code: "Something", + is_dynamic: true, + rule: { + type: "percentage", + allocation: "total", + value: 10, + }, + regions: [IdMap.getId("region-france")], + }, + total10Percent: { + id: IdMap.getId("total10"), + code: "10%OFF", + rule: { + type: "percentage", + allocation: "total", + value: 10, + }, + regions: [IdMap.getId("region-france")], + }, + item10Percent: { + id: IdMap.getId("item10Percent"), + code: "MEDUSA", + rule: { + type: "percentage", + allocation: "item", + value: 10, + valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")], + }, + regions: [IdMap.getId("region-france")], + }, + total10Fixed: { + id: IdMap.getId("total10Fixed"), + code: "MEDUSA", + rule: { + type: "fixed", + allocation: "total", + value: 10, + }, + regions: [IdMap.getId("region-france")], + }, + item9Fixed: { + id: IdMap.getId("item9Fixed"), + code: "MEDUSA", + rule: { + type: "fixed", + allocation: "item", + value: 9, + valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")], + }, + regions: [IdMap.getId("region-france")], + }, + item2Fixed: { + id: IdMap.getId("item2Fixed"), + code: "MEDUSA", + rule: { + type: "fixed", + allocation: "item", + value: 2, + valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")], + }, + regions: [IdMap.getId("region-france")], + }, + item10FixedNoVariants: { + id: IdMap.getId("item10FixedNoVariants"), + code: "MEDUSA", + rule: { + type: "fixed", + allocation: "item", + value: 10, + valid_for: [], + }, + regions: [IdMap.getId("region-france")], + }, + expiredDiscount: { + id: IdMap.getId("expired"), + code: "MEDUSA", + ends_at: new Date("December 17, 1995 03:24:00"), + rule: { + type: "fixed", + allocation: "item", + value: 10, + valid_for: [], + }, + regions: [IdMap.getId("region-france")], + }, + freeShipping: { + id: IdMap.getId("freeshipping"), + code: "FREESHIPPING", + rule: { + type: "free_shipping", + allocation: "total", + value: 10, + valid_for: [], + }, + regions: [IdMap.getId("region-france")], + }, + USDiscount: { + id: IdMap.getId("us-discount"), + code: "US10", + rule: { + type: "free_shipping", + allocation: "total", + value: 10, + valid_for: [], + }, + regions: [IdMap.getId("us")], + }, + alreadyExists: { + code: "ALREADYEXISTS", + rule: { + type: "percentage", + allocation: "total", + value: 20, + }, + regions: [IdMap.getId("fr-cart")], + }, +} export const DiscountServiceMock = { + withTransaction: function() { + return this + }, create: jest.fn().mockImplementation(data => { return Promise.resolve(data) }), @@ -34,7 +157,7 @@ export const DiscountServiceMock = { }), delete: jest.fn().mockImplementation(data => { return Promise.resolve({ - _id: IdMap.getId("total10"), + id: IdMap.getId("total10"), object: "discount", deleted: true, }) @@ -42,16 +165,13 @@ export const DiscountServiceMock = { list: jest.fn().mockImplementation(data => { return Promise.resolve([{}]) }), - decorate: jest.fn().mockImplementation(data => { - return Promise.resolve(data) - }), addRegion: jest.fn().mockReturnValue(Promise.resolve()), removeRegion: jest.fn().mockReturnValue(Promise.resolve()), - addValidVariant: jest.fn().mockReturnValue(Promise.resolve()), - removeValidVariant: jest.fn().mockReturnValue(Promise.resolve()), + addValidProduct: jest.fn().mockReturnValue(Promise.resolve()), + removeValidProduct: jest.fn().mockReturnValue(Promise.resolve()), generateGiftCard: jest.fn().mockReturnValue( Promise.resolve({ - _id: IdMap.getId("gift_card_id"), + id: IdMap.getId("gift_card_id"), }) ), } diff --git a/packages/medusa/src/services/__mocks__/fulfillment-provider.js b/packages/medusa/src/services/__mocks__/fulfillment-provider.js index 0c1b65fcbf..762856305a 100644 --- a/packages/medusa/src/services/__mocks__/fulfillment-provider.js +++ b/packages/medusa/src/services/__mocks__/fulfillment-provider.js @@ -64,6 +64,9 @@ export const FulfillmentProviderServiceMock = { } throw new Error("Provider Not Found") }), + list: jest.fn().mockImplementation(() => { + return Promise.resolve() + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__mocks__/idempotency-key.js b/packages/medusa/src/services/__mocks__/idempotency-key.js new file mode 100644 index 0000000000..51b26fdec2 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/idempotency-key.js @@ -0,0 +1,42 @@ +import { MockManager } from "medusa-test-utils" + +export const IdempotencyKeyService = { + withTransaction: function() { + return this + }, + initializeRequest: jest.fn().mockImplementation(() => { + return { + idempotency_key: "testkey", + recovery_point: "started", + } + }), + workStage: jest.fn().mockImplementation(async (key, fn) => { + try { + const { recovery_point, response_code, response_body } = await fn( + MockManager + ) + + if (recovery_point) { + return { + key: { recovery_point }, + } + } else { + return { + key: { + recovery_point: "finished", + response_body, + response_code, + }, + } + } + } catch (err) { + return { error: err } + } + }), +} + +const mock = jest.fn().mockImplementation(() => { + return IdempotencyKeyService +}) + +export default mock diff --git a/packages/medusa/src/services/__mocks__/line-item.js b/packages/medusa/src/services/__mocks__/line-item.js index c0e0822e12..ff86faee56 100644 --- a/packages/medusa/src/services/__mocks__/line-item.js +++ b/packages/medusa/src/services/__mocks__/line-item.js @@ -2,6 +2,15 @@ import { IdMap } from "medusa-test-utils" import { MedusaError } from "medusa-core-utils" export const LineItemServiceMock = { + withTransaction: function() { + return this + }, + create: jest.fn().mockImplementation(data => { + return Promise.resolve({ ...data }) + }), + update: jest.fn().mockImplementation(data => { + return Promise.resolve({ ...data }) + }), validate: jest.fn().mockImplementation(data => { if (data.title === "invalid lineitem") { throw new Error(`"content" is required`) @@ -16,13 +25,13 @@ export const LineItemServiceMock = { ) { return line.content.every( (c, index) => - c.variant._id === match[index].variant._id && + c.variant.id === match[index].variant.id && c.quantity === match[index].quantity ) } } else if (!Array.isArray(match.content)) { return ( - line.content.variant._id === match.content.variant._id && + line.content.variant.id === match.content.variant.id && line.content.quantity === match.content.quantity ) } @@ -40,16 +49,8 @@ export const LineItemServiceMock = { } return Promise.resolve({ - content: { - variant: { - _id: variantId, - }, - product: { - _id: `p_${variantId}`, - }, - quantity: 1, - unit_price: 100, - }, + variant_id: variantId, + unit_price: 100, quantity, metadata, }) diff --git a/packages/medusa/src/services/__mocks__/order.js b/packages/medusa/src/services/__mocks__/order.js index d7909d3cfa..e0f6bcd3f8 100644 --- a/packages/medusa/src/services/__mocks__/order.js +++ b/packages/medusa/src/services/__mocks__/order.js @@ -2,7 +2,7 @@ import { IdMap } from "medusa-test-utils" export const orders = { testOrder: { - _id: IdMap.getId("test-order"), + id: IdMap.getId("test-order"), email: "virgil@vandijk.dk", billing_address: { first_name: "Virgil", @@ -24,40 +24,40 @@ export const orders = { }, items: [ { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 123, variant: { - _id: IdMap.getId("can-cover"), + id: IdMap.getId("can-cover"), }, product: { - _id: IdMap.getId("validId"), + id: IdMap.getId("validId"), }, quantity: 1, }, quantity: 10, }, ], - region_id: IdMap.getId("testRegion"), - customer_id: IdMap.getId("testCustomer"), + regionid: IdMap.getId("testRegion"), + customerid: IdMap.getId("testCustomer"), payment_method: { - provider_id: "default_provider", + providerid: "default_provider", data: {}, }, shipping_method: [ { - provider_id: "default_provider", - profile_id: IdMap.getId("validId"), + providerid: "default_provider", + profileid: IdMap.getId("validId"), data: {}, items: {}, }, ], }, processedOrder: { - _id: IdMap.getId("processed-order"), + id: IdMap.getId("processed-order"), email: "oliver@test.dk", billing_address: { first_name: "Oli", @@ -77,42 +77,42 @@ export const orders = { }, items: [ { - _id: IdMap.getId("existingLine"), + id: IdMap.getId("existingLine"), title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { unit_price: 123, variant: { - _id: IdMap.getId("can-cover"), + id: IdMap.getId("can-cover"), }, product: { - _id: IdMap.getId("validId"), + id: IdMap.getId("validId"), }, quantity: 1, }, quantity: 10, }, ], - region_id: IdMap.getId("region-france"), - customer_id: IdMap.getId("test-customer"), + regionid: IdMap.getId("region-france"), + customerid: IdMap.getId("test-customer"), payment_method: { - provider_id: "default_provider", + providerid: "default_provider", }, shipping_methods: [ { - _id: IdMap.getId("expensiveShipping"), + id: IdMap.getId("expensiveShipping"), name: "Expensive Shipping", price: 100, - provider_id: "default_provider", - profile_id: IdMap.getId("default"), + providerid: "default_provider", + profileid: IdMap.getId("default"), }, { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), name: "Free Shipping", price: 10, - provider_id: "default_provider", - profile_id: IdMap.getId("profile1"), + providerid: "default_provider", + profileid: IdMap.getId("profile1"), }, ], tax_rate: 0, @@ -123,6 +123,9 @@ export const orders = { } export const OrderServiceMock = { + withTransaction: function() { + return this + }, create: jest.fn().mockImplementation(data => { return Promise.resolve(orders.testOrder) }), @@ -160,7 +163,7 @@ export const OrderServiceMock = { return Promise.resolve(undefined) }), retrieveByCartId: jest.fn().mockImplementation(cartId => { - return Promise.resolve() + return Promise.resolve({ id: IdMap.getId("test-order") }) }), decorate: jest.fn().mockImplementation(order => { order.decorated = true diff --git a/packages/medusa/src/services/__mocks__/payment-provider.js b/packages/medusa/src/services/__mocks__/payment-provider.js index 3ff775b6ce..3d98eb6cbe 100644 --- a/packages/medusa/src/services/__mocks__/payment-provider.js +++ b/packages/medusa/src/services/__mocks__/payment-provider.js @@ -13,6 +13,9 @@ export const DefaultProviderMock = { retrievePayment: jest.fn().mockImplementation(data => { return Promise.resolve(data) }), + list: jest.fn().mockImplementation(() => { + return Promise.resolve() + }), capturePayment: jest.fn().mockReturnValue(Promise.resolve()), refundPayment: jest.fn().mockReturnValue(Promise.resolve()), cancelPayment: jest.fn().mockReturnValue(Promise.resolve({})), @@ -25,6 +28,9 @@ export const PaymentProviderServiceMock = { id: `${session.data.id}_updated`, }) }), + list: jest.fn().mockImplementation(() => { + return Promise.resolve() + }), createSession: jest.fn().mockImplementation((providerId, cart) => { return Promise.resolve({ id: `${providerId}_session`, diff --git a/packages/medusa/src/services/__mocks__/product-variant.js b/packages/medusa/src/services/__mocks__/product-variant.js index 3de7a30777..e1cced6d78 100644 --- a/packages/medusa/src/services/__mocks__/product-variant.js +++ b/packages/medusa/src/services/__mocks__/product-variant.js @@ -1,7 +1,7 @@ import { IdMap } from "medusa-test-utils" const variant1 = { - _id: "1", + id: "1", title: "variant1", options: [ { @@ -16,7 +16,7 @@ const variant1 = { } const variant2 = { - _id: "2", + id: "2", title: "variant2", options: [ { @@ -31,7 +31,7 @@ const variant2 = { } const variant3 = { - _id: "3", + id: "3", title: "variant3", options: [ { @@ -46,7 +46,7 @@ const variant3 = { } const variant4 = { - _id: "4", + id: "4", title: "variant4", options: [ { @@ -61,7 +61,7 @@ const variant4 = { } const variant5 = { - _id: "5", + id: "5", title: "Variant with valid id", options: [ { @@ -76,7 +76,7 @@ const variant5 = { } const invalidVariant = { - _id: "invalid_option", + id: "invalid_option", title: "variant3", options: [ { @@ -91,23 +91,23 @@ const invalidVariant = { } const testVariant = { - _id: IdMap.getId("testVariant"), + id: IdMap.getId("testVariant"), title: "test variant", } const emptyVariant = { - _id: "empty_option", + id: "empty_option", title: "variant3", options: [], } const eur10us12 = { - _id: IdMap.getId("eur-10-us-12"), + id: IdMap.getId("eur-10-us-12"), title: "EUR10US-12", } const giftCardVar = { - _id: IdMap.getId("giftCardVar"), + id: IdMap.getId("giftCardVar"), title: "100 USD", } @@ -124,12 +124,15 @@ export const variants = { } export const ProductVariantServiceMock = { - createDraft: jest.fn().mockImplementation(data => { + withTransaction: function() { + return this + }, + create: jest.fn().mockImplementation(data => { return Promise.resolve(testVariant) }), publish: jest.fn().mockImplementation(_ => { return Promise.resolve({ - _id: IdMap.getId("publish"), + id: IdMap.getId("publish"), name: "Product Variant", published: true, }) @@ -212,25 +215,6 @@ export const ProductVariantServiceMock = { return Promise.resolve({}) }), list: jest.fn().mockImplementation(data => { - if (data._id && data._id.$in) { - return Promise.resolve( - data._id.$in.map(id => { - if (id === "1") { - return variant1 - } - if (id === "2") { - return variant2 - } - if (id === "3") { - return variant3 - } - if (id === "4") { - return variant4 - } - }) - ) - } - return Promise.resolve([testVariant]) }), deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => { diff --git a/packages/medusa/src/services/__mocks__/product.js b/packages/medusa/src/services/__mocks__/product.js index 1552d6f7dd..82fcff22ae 100644 --- a/packages/medusa/src/services/__mocks__/product.js +++ b/packages/medusa/src/services/__mocks__/product.js @@ -1,27 +1,26 @@ import { IdMap } from "medusa-test-utils" -import { MedusaError } from "medusa-core-utils" export const products = { product1: { - _id: IdMap.getId("product1"), + id: IdMap.getId("product1"), title: "Product 1", }, publishProduct: { - _id: IdMap.getId("publish"), + id: IdMap.getId("publish"), title: "Product 1", published: true, }, product2: { - _id: IdMap.getId("product2"), + id: IdMap.getId("product2"), title: "Product 2", }, productWithOptions: { - _id: IdMap.getId("productWithOptions"), + id: IdMap.getId("productWithOptions"), title: "Test", variants: [IdMap.getId("variant1")], options: [ { - _id: IdMap.getId("option1"), + id: IdMap.getId("option1"), title: "Test", values: [IdMap.getId("optionValue1")], }, @@ -30,7 +29,10 @@ export const products = { } export const ProductServiceMock = { - createDraft: jest.fn().mockImplementation(data => { + withTransaction: function() { + return this + }, + create: jest.fn().mockImplementation(data => { if (data.title === "Test Product") { return Promise.resolve(products.product1) } @@ -40,7 +42,7 @@ export const ProductServiceMock = { count: jest.fn().mockReturnValue(4), publish: jest.fn().mockImplementation(_ => { return Promise.resolve({ - _id: IdMap.getId("publish"), + id: IdMap.getId("publish"), name: "Product 1", published: true, }) @@ -71,7 +73,7 @@ export const ProductServiceMock = { retrieveVariants: jest .fn() .mockReturnValue( - Promise.resolve([{ _id: IdMap.getId("1") }, { _id: IdMap.getId("2") }]) + Promise.resolve([{ id: IdMap.getId("1") }, { id: IdMap.getId("2") }]) ), retrieve: jest.fn().mockImplementation(productId => { if (productId === IdMap.getId("product1")) { @@ -81,7 +83,7 @@ export const ProductServiceMock = { return Promise.resolve(products.product2) } if (productId === IdMap.getId("validId")) { - return Promise.resolve({ _id: IdMap.getId("validId") }) + return Promise.resolve({ id: IdMap.getId("validId") }) } if (productId === IdMap.getId("publish")) { return Promise.resolve(products.publishProduct) @@ -100,7 +102,7 @@ export const ProductServiceMock = { if (data.variants === IdMap.getId("giftCardVar")) { return Promise.resolve([ { - _id: IdMap.getId("giftCardProd"), + id: IdMap.getId("giftCardProd"), title: "Gift Card", is_giftcard: true, thumbnail: "1234", @@ -110,11 +112,11 @@ export const ProductServiceMock = { if (data.variants === IdMap.getId("testVariant")) { return Promise.resolve([ { - _id: "1234", + id: "1234", title: "test", options: [ { - _id: IdMap.getId("testOptionId"), + id: IdMap.getId("testOptionId"), title: "testOption", }, ], @@ -124,7 +126,7 @@ export const ProductServiceMock = { if (data.variants === IdMap.getId("eur-10-us-12")) { return Promise.resolve([ { - _id: "1234", + id: "1234", title: "test", thumbnail: "test.1234", }, diff --git a/packages/medusa/src/services/__mocks__/region.js b/packages/medusa/src/services/__mocks__/region.js index 3df3e94cb9..8909e3ef06 100644 --- a/packages/medusa/src/services/__mocks__/region.js +++ b/packages/medusa/src/services/__mocks__/region.js @@ -2,7 +2,7 @@ import { IdMap } from "medusa-test-utils" export const regions = { testRegion: { - _id: IdMap.getId("testRegion"), + id: IdMap.getId("testRegion"), name: "Test Region", countries: ["DK", "US", "DE"], tax_rate: 0.25, @@ -11,7 +11,7 @@ export const regions = { currency_code: "usd", }, regionFrance: { - _id: IdMap.getId("region-france"), + id: IdMap.getId("region-france"), name: "France", countries: ["FR"], payment_providers: ["default_provider", "france-provider"], @@ -20,21 +20,21 @@ export const regions = { tax_rate: 0.25, }, regionUs: { - _id: IdMap.getId("region-us"), + id: IdMap.getId("region-us"), tax_rate: 0.25, name: "USA", countries: ["US"], currency_code: "usd", }, regionGermany: { - _id: IdMap.getId("region-de"), + id: IdMap.getId("region-de"), tax_rate: 0.25, name: "Germany", countries: ["DE"], currency_code: "eur", }, regionSweden: { - _id: IdMap.getId("region-se"), + id: IdMap.getId("region-se"), tax_rate: 0.25, name: "Sweden", countries: ["SE"], @@ -59,10 +59,12 @@ export const RegionServiceMock = { if (regionId === IdMap.getId("region-se")) { return Promise.resolve(regions.regionSweden) } - throw Error(regionId + "not found") + return Promise.resolve(regions.testRegion) }), delete: jest.fn().mockImplementation(data => Promise.resolve()), - create: jest.fn().mockImplementation(data => Promise.resolve()), + create: jest + .fn() + .mockImplementation(data => Promise.resolve({ id: "region" })), addCountry: jest.fn().mockImplementation(data => Promise.resolve()), addFulfillmentProvider: jest .fn() diff --git a/packages/medusa/src/services/__mocks__/return.js b/packages/medusa/src/services/__mocks__/return.js new file mode 100644 index 0000000000..4e6875e418 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/return.js @@ -0,0 +1,14 @@ +export const ReturnService = { + withTransaction: function() { + return this + }, + create: jest.fn(() => Promise.resolve({ id: "return" })), + fulfill: jest.fn(), + update: jest.fn(), +} + +const mock = jest.fn().mockImplementation(() => { + return ReturnService +}) + +export default mock diff --git a/packages/medusa/src/services/__mocks__/shipping-profile.js b/packages/medusa/src/services/__mocks__/shipping-profile.js index e7f80ae1fb..d4e3848136 100644 --- a/packages/medusa/src/services/__mocks__/shipping-profile.js +++ b/packages/medusa/src/services/__mocks__/shipping-profile.js @@ -2,13 +2,13 @@ import { IdMap } from "medusa-test-utils" export const profiles = { default: { - _id: IdMap.getId("default"), + id: IdMap.getId("default"), name: "default_profile", products: [IdMap.getId("product")], shipping_options: [], }, other: { - _id: IdMap.getId("profile1"), + id: IdMap.getId("profile1"), name: "other_profile", products: [IdMap.getId("product")], shipping_options: [], @@ -32,10 +32,10 @@ export const ShippingProfileServiceMock = { return Promise.resolve(profiles.default) }), retrieveGiftCardDefault: jest.fn().mockImplementation(data => { - return Promise.resolve({ _id: IdMap.getId("giftCardProfile") }) + return Promise.resolve({ id: IdMap.getId("giftCardProfile") }) }), retrieveDefault: jest.fn().mockImplementation(data => { - return Promise.resolve({ _id: IdMap.getId("default_shipping_profile") }) + return Promise.resolve({ id: IdMap.getId("default_shipping_profile") }) }), list: jest.fn().mockImplementation(selector => { if (!selector) { @@ -45,10 +45,10 @@ export const ShippingProfileServiceMock = { return Promise.resolve([]) } if (selector.shipping_options === IdMap.getId("freeShipping")) { - return Promise.resolve([{ _id: IdMap.getId("default_profile") }]) + return Promise.resolve([{ id: IdMap.getId("default_profile") }]) } if (selector.shipping_options === IdMap.getId("additional")) { - return Promise.resolve([{ _id: IdMap.getId("additional_profile") }]) + return Promise.resolve([{ id: IdMap.getId("additional_profile") }]) } if ( selector.products && @@ -60,7 +60,7 @@ export const ShippingProfileServiceMock = { products: [IdMap.getId("product")], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), name: "Free Shipping", region_id: IdMap.getId("testRegion"), price: { @@ -88,7 +88,7 @@ export const ShippingProfileServiceMock = { products: [IdMap.getId("product1")], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), name: "Free Shipping", region_id: IdMap.getId("testRegion"), price: { @@ -108,7 +108,7 @@ export const ShippingProfileServiceMock = { products: [IdMap.getId("product2")], shipping_options: [ { - _id: IdMap.getId("freeShipping"), + id: IdMap.getId("freeShipping"), name: "Free French Shipping", region_id: IdMap.getId("region-france"), price: { @@ -132,7 +132,10 @@ export const ShippingProfileServiceMock = { addProduct: jest.fn().mockImplementation(() => Promise.resolve()), removeProduct: jest.fn().mockImplementation(() => Promise.resolve()), fetchCartOptions: jest.fn().mockImplementation(() => { - return Promise.resolve([{ _id: IdMap.getId("cartShippingOption") }]) + return Promise.resolve([{ id: IdMap.getId("cartShippingOption") }]) + }), + fetchOptionsByProductIds: jest.fn().mockImplementation(() => { + return Promise.resolve([{ id: IdMap.getId("cartShippingOption") }]) }), } diff --git a/packages/medusa/src/services/__mocks__/swap.js b/packages/medusa/src/services/__mocks__/swap.js new file mode 100644 index 0000000000..2ad66283d7 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/swap.js @@ -0,0 +1,22 @@ +import { IdMap } from "medusa-test-utils" + +export const SwapServiceMock = { + withTransaction: function() { + return this + }, + registerCartCompletion: jest.fn().mockImplementation(data => { + return Promise.resolve({ id: "test-swap" }) + }), + create: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), + retrieve: jest.fn().mockImplementation(data => { + return Promise.resolve({ id: "test-swap" }) + }), +} + +const mock = jest.fn().mockImplementation(() => { + return SwapServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 7f25b5c01b..b268ff4740 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -1,50 +1,74 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import _ from "lodash" +import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import CartService from "../cart" -import { - PaymentProviderServiceMock, - DefaultProviderMock, -} from "../__mocks__/payment-provider" -import { ProductVariantServiceMock } from "../__mocks__/product-variant" -import { RegionServiceMock } from "../__mocks__/region" -import { EventBusServiceMock } from "../__mocks__/event-bus" -import { CustomerServiceMock } from "../__mocks__/customer" -import { ShippingOptionServiceMock } from "../__mocks__/shipping-option" -import { TotalsServiceMock } from "../__mocks__/totals" -import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" -import { CartModelMock, carts } from "../../models/__mocks__/cart" -import { LineItemServiceMock } from "../__mocks__/line-item" -import { DiscountModelMock, discounts } from "../../models/__mocks__/discount" -import { DiscountServiceMock } from "../__mocks__/discount" -import idMap from "medusa-test-utils/dist/id-map" + +const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, +} describe("CartService", () => { + const totalsService = { + getTotal: o => { + return o.total || 0 + }, + getSubtotal: o => { + return o.subtotal || 0 + }, + getTaxTotal: o => { + return o.tax_total || 0 + }, + getDiscountTotal: o => { + return o.discount_total || 0 + }, + getShippingTotal: o => { + return o.shipping_total || 0 + }, + getGiftCardTotal: o => { + return o.gift_card_total || 0 + }, + } + describe("retrieve", () => { let result + const cartRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("emptyCart") }), + }) beforeAll(async () => { jest.clearAllMocks() const cartService = new CartService({ - cartModel: CartModelMock, + manager: MockManager, + totalsService, + cartRepository, }) result = await cartService.retrieve(IdMap.getId("emptyCart")) }) it("calls cart model functions", () => { - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("emptyCart"), + expect(cartRepository.findOne).toHaveBeenCalledTimes(1) + expect(cartRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("emptyCart") }, }) }) - - it("returns the cart", () => { - expect(result).toEqual(carts.emptyCart) - }) }) describe("setMetadata", () => { + const cartRepository = MockRepository({ + findOne: () => { + return Promise.resolve({ + metadata: { + existing: "something", + }, + }) + }, + }) const cartService = new CartService({ - cartModel: CartModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + eventBusService, }) beforeEach(() => { @@ -52,27 +76,31 @@ describe("CartService", () => { }) it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await cartService.setMetadata(`${id}`, "metadata", "testMetadata") + const id = "testCart" + await cartService.setMetadata(id, "metadata", "testMetadata") - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toBeCalledTimes(1) - expect(CartModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } - ) + expect(cartRepository.findOne).toBeCalledTimes(1) + expect(cartRepository.findOne).toBeCalledWith(id) + + expect(cartRepository.save).toBeCalledTimes(1) + expect(cartRepository.save).toBeCalledWith({ + metadata: { + existing: "something", + metadata: "testMetadata", + }, + }) }) it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() - + const id = "testCart" try { - await cartService.setMetadata(`${id}`, 1234, "nono") + await cartService.setMetadata(id, 1234, "nono") } catch (err) { expect(err.message).toEqual( "Key type is invalid. Metadata keys must be strings" @@ -82,9 +110,25 @@ describe("CartService", () => { }) describe("deleteMetadata", () => { + const cartRepository = MockRepository({ + findOne: id => { + if (id === "empty") { + return Promise.resolve({ + metadata: {}, + }) + } + return Promise.resolve({ + metadata: { + existing: "something", + }, + }) + }, + }) const cartService = new CartService({ - cartModel: CartModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + eventBusService, }) beforeEach(() => { @@ -92,27 +136,46 @@ describe("CartService", () => { }) it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await cartService.deleteMetadata(`${id}`, "metadata") + const id = "testCart" + await cartService.deleteMetadata(id, "existing") - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toBeCalledTimes(1) - expect(CartModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $unset: { "metadata.metadata": "" } } + expect(cartRepository.findOne).toBeCalledTimes(1) + expect(cartRepository.findOne).toBeCalledWith(id) + + expect(cartRepository.save).toBeCalledTimes(1) + expect(cartRepository.save).toBeCalledWith({ + metadata: {}, + }) + }) + + it("works when metadata is empty", async () => { + const id = "empty" + await cartService.deleteMetadata(id, "existing") + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "cart.updated", + expect.any(Object) ) + + expect(cartRepository.findOne).toBeCalledTimes(1) + expect(cartRepository.findOne).toBeCalledWith(id) + + expect(cartRepository.save).toBeCalledTimes(1) + expect(cartRepository.save).toBeCalledWith({ + metadata: {}, + }) }) it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() - try { - await cartService.deleteMetadata(`${id}`, 1234) + await cartService.deleteMetadata("testCart", 1234) } catch (err) { expect(err.message).toEqual( "Key type is invalid. Metadata keys must be strings" @@ -122,10 +185,24 @@ describe("CartService", () => { }) describe("create", () => { + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("testRegion"), + countries: [{ iso_2: "us" }], + } + }, + } + + const addressRepository = MockRepository({ create: c => c }) + const cartRepository = MockRepository() const cartService = new CartService({ - cartModel: CartModelMock, - regionService: RegionServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + addressRepository, + totalsService, + cartRepository, + regionService, + eventBusService, }) beforeEach(() => { @@ -137,16 +214,26 @@ describe("CartService", () => { region_id: IdMap.getId("testRegion"), }) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.created", expect.any(Object) ) - expect(CartModelMock.create).toHaveBeenCalledTimes(1) - expect(CartModelMock.create).toHaveBeenCalledWith({ - region_id: IdMap.getId("testRegion"), + expect(addressRepository.create).toHaveBeenCalledTimes(1) + expect(addressRepository.create).toHaveBeenCalledWith({ + country_code: "us", }) + + expect(cartRepository.create).toHaveBeenCalledTimes(1) + expect(cartRepository.create).toHaveBeenCalledWith({ + region_id: IdMap.getId("testRegion"), + shipping_address: { + country_code: "us", + }, + }) + + expect(cartRepository.save).toHaveBeenCalledTimes(1) }) it("creates a cart with a prefilled shipping address", async () => { @@ -159,7 +246,7 @@ describe("CartService", () => { city: "Dunkville", province: "CA", postal_code: "12345", - country_code: "PT", + country_code: "pt", }, }) @@ -176,18 +263,18 @@ describe("CartService", () => { city: "Dunkville", province: "CA", postal_code: "12345", - country_code: "US", + country_code: "us", }, }) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.created", expect.any(Object) ) - expect(CartModelMock.create).toHaveBeenCalledTimes(1) - expect(CartModelMock.create).toHaveBeenCalledWith({ + expect(cartRepository.create).toHaveBeenCalledTimes(1) + expect(cartRepository.create).toHaveBeenCalledWith({ region_id: IdMap.getId("testRegion"), shipping_address: { first_name: "LeBron", @@ -196,18 +283,71 @@ describe("CartService", () => { city: "Dunkville", province: "CA", postal_code: "12345", - country_code: "US", + country_code: "us", }, }) + + expect(cartRepository.save).toHaveBeenCalledTimes(1) }) }) describe("addLineItem", () => { + const lineItemService = { + update: jest.fn(), + create: jest.fn(), + withTransaction: function() { + return this + }, + } + + const shippingOptionService = { + deleteShippingMethod: jest.fn(), + withTransaction: function() { + return this + }, + } + + const productVariantService = { + canCoverQuantity: jest + .fn() + .mockImplementation(id => id !== IdMap.getId("cannot-cover")), + } + + const cartRepository = MockRepository({ + findOne: q => { + if (q.where.id === IdMap.getId("cartWithLine")) { + return Promise.resolve({ + items: [ + { + id: IdMap.getId("merger"), + title: "will merge", + variant_id: IdMap.getId("existing"), + should_merge: true, + quantity: 1, + }, + ], + }) + } + return Promise.resolve({ + shipping_methods: [ + { + shipping_option: { + profile_id: IdMap.getId("testProfile"), + }, + }, + ], + items: [], + }) + }, + }) const cartService = new CartService({ - cartModel: CartModelMock, - productVariantService: ProductVariantServiceMock, - lineItemService: LineItemServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + lineItemService, + productVariantService, + eventBusService, + shippingOptionService, }) beforeEach(() => { @@ -219,41 +359,57 @@ describe("CartService", () => { title: "New Line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, + variant_id: IdMap.getId("can-cover"), + unit_price: 123, quantity: 10, } - await cartService.addLineItem(IdMap.getId("emptyCart"), lineItem) + await cartService.addLineItem(IdMap.getId("emptyCart"), _.clone(lineItem)) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("emptyCart"), - }, - { - $push: { - items: { - ...lineItem, - has_shipping: false, - }, + expect(lineItemService.create).toHaveBeenCalledTimes(1) + expect(lineItemService.create).toHaveBeenCalledWith({ + ...lineItem, + has_shipping: false, + cart_id: IdMap.getId("emptyCart"), + }) + }) + + it("successfully creates new line item with shipping", async () => { + const lineItem = { + title: "New Line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + should_merge: true, + variant_id: IdMap.getId("can-cover"), + variant: { + product: { + profile_id: IdMap.getId("testProfile"), }, - } + }, + unit_price: 123, + quantity: 10, + } + + await cartService.addLineItem(IdMap.getId("emptyCart"), _.clone(lineItem)) + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "cart.updated", + expect.any(Object) ) + + expect(lineItemService.create).toHaveBeenCalledTimes(1) + expect(lineItemService.create).toHaveBeenCalledWith({ + ...lineItem, + has_shipping: false, + cart_id: IdMap.getId("emptyCart"), + }) }) it("successfully merges existing line item", async () => { @@ -261,93 +417,19 @@ describe("CartService", () => { title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - quantity: 10, + unit_price: 123, + variant_id: IdMap.getId("existing"), + should_merge: true, + quantity: 1, } await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( + expect(lineItemService.update).toHaveBeenCalledTimes(2) + expect(lineItemService.update).toHaveBeenCalledWith( + IdMap.getId("merger"), { - _id: IdMap.getId("cartWithLine"), - }, - { - $push: { - items: { - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - has_shipping: false, - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - quantity: 10, - }, - }, - } - ) - }) - - it("successfully adds multi-content line", async () => { - const lineItem = { - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: [ - { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - ], - quantity: 10, - } - - await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) - - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), - }, - { - $push: { - items: { - ...lineItem, - has_shipping: false, - }, - }, + quantity: 2, } ) }) @@ -358,73 +440,87 @@ describe("CartService", () => { description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", quantity: 1, - content: { - variant: { - _id: IdMap.getId("cannot-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - unit_price: 1234, - }, + variant_id: IdMap.getId("cannot-cover"), } - try { - await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) - } catch (err) { - expect(err.message).toEqual( - `Inventory doesn't cover the desired quantity` - ) - } + await expect( + cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) + ).rejects.toThrow(`Inventory doesn't cover the desired quantity`) }) - it("throws if inventory isn't covered multi-line", async () => { + it("throws if inventory isn't covered", async () => { const lineItem = { title: "merge line", description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", quantity: 1, - content: [ - { - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - unit_price: 1234, - }, - { - variant: { - _id: IdMap.getId("cannot-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - unit_price: 1234, - }, - ], + variant_id: IdMap.getId("cannot-cover"), } - try { - await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) - } catch (err) { - expect(err.message).toEqual( - `Inventory doesn't cover the desired quantity` - ) - } + await expect( + cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) + ).rejects.toThrow(`Inventory doesn't cover the desired quantity`) }) }) describe("removeLineItem", () => { + const lineItemService = { + delete: jest.fn(), + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const cartRepository = MockRepository({ + findOne: q => { + if (q.where.id === IdMap.getId("withShipping")) { + return Promise.resolve({ + shipping_methods: [ + { + id: IdMap.getId("ship-method"), + shipping_option: { + profile_id: IdMap.getId("prevPro"), + }, + }, + ], + items: [ + { + id: IdMap.getId("itemToRemove"), + variant_id: IdMap.getId("existing"), + variant: { + product: { + profile_id: IdMap.getId("prevPro"), + }, + }, + }, + ], + }) + } + return Promise.resolve({ + shipping_methods: [], + items: [ + { + id: IdMap.getId("itemToRemove"), + }, + ], + }) + }, + }) + + const shippingOptionService = { + deleteShippingMethod: jest.fn(), + withTransaction: function() { + return this + }, + } + const cartService = new CartService({ - cartModel: CartModelMock, - productVariantService: ProductVariantServiceMock, - lineItemService: LineItemServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + lineItemService, + shippingOptionService, + eventBusService, }) beforeEach(() => { @@ -437,44 +533,33 @@ describe("CartService", () => { IdMap.getId("itemToRemove") ) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( - "cart.updated", - expect.any(Object) + expect(lineItemService.delete).toHaveBeenCalledTimes(1) + expect(lineItemService.delete).toHaveBeenCalledWith( + IdMap.getId("itemToRemove") ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), - }, - { - $pull: { items: { _id: IdMap.getId("itemToRemove") } }, - } + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "cart.updated", + expect.any(Object) ) }) - it("successfully decrements quantity if more than 1", async () => { + it("removes shipping method if not necessary", async () => { await cartService.removeLineItem( - IdMap.getId("cartWithLine"), - IdMap.getId("existingLine") + IdMap.getId("withShipping"), + IdMap.getId("itemToRemove") ) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( - "cart.updated", - expect.any(Object) + expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledTimes( + 1 ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), + expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledWith({ + id: IdMap.getId("ship-method"), + shipping_option: { + profile_id: IdMap.getId("prevPro"), }, - { - $pull: { items: { _id: IdMap.getId("existingLine") } }, - } - ) + }) }) it("resolves if line item is not in cart", async () => { @@ -483,16 +568,104 @@ describe("CartService", () => { IdMap.getId("nonExisting") ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(0) + expect(lineItemService.delete).toHaveBeenCalledTimes(0) + }) + }) + + describe("update", () => { + const cartRepository = MockRepository({ + findOne: q => { + if (q.where.id === "withpays") { + return Promise.resolve({ + payment_sessions: [ + { + id: "test", + }, + ], + }) + } + }, + }) + + const cartService = new CartService({ + manager: MockManager, + cartRepository, + totalsService, + eventBusService, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("retrieves correctly", async () => { + cartService.setPaymentSessions = jest.fn() + await cartService.update("withpays", {}) + + expect(cartRepository.findOne).toHaveBeenCalledWith({ + relations: [ + "items", + "shipping_methods", + "shipping_address", + "billing_address", + "gift_cards", + "discounts", + "customer", + "region", + "payment_sessions", + "region.countries", + "discounts.rule", + "discounts.regions", + ], + where: { id: "withpays" }, + }) }) }) describe("updateLineItem", () => { + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const productVariantService = { + canCoverQuantity: jest + .fn() + .mockImplementation(id => id !== IdMap.getId("cannot-cover")), + } + + const cartRepository = MockRepository({ + findOne: q => { + if (q.where.id === IdMap.getId("cannot")) { + return Promise.resolve({ + items: [ + { + id: IdMap.getId("existing"), + variant_id: IdMap.getId("cannot-cover"), + quantity: 1, + }, + ], + }) + } + return Promise.resolve({ + items: [ + { + id: IdMap.getId("existing"), + variant_id: IdMap.getId("good"), + quantity: 1, + }, + ], + }) + }, + }) const cartService = new CartService({ - cartModel: CartModelMock, - productVariantService: ProductVariantServiceMock, - lineItemService: LineItemServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + productVariantService, + lineItemService, + eventBusService, }) beforeEach(() => { @@ -500,84 +673,60 @@ describe("CartService", () => { }) it("successfully updates existing line item", async () => { - const lineItem = { - title: "update line", - description: "This is a new line", - thumbnail: "https://test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - quantity: 2, - } - await cartService.updateLineItem( IdMap.getId("cartWithLine"), - IdMap.getId("existingLine"), - lineItem + IdMap.getId("existing"), + { quantity: 2 } ) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), - "items._id": IdMap.getId("existingLine"), - }, - { - $set: { "items.$": lineItem }, - } + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith( + IdMap.getId("existing"), + { quantity: 2 } ) }) it("throws if inventory isn't covered", async () => { - const lineItem = { - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - quantity: 1, - content: { - variant: { - _id: IdMap.getId("cannot-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - unit_price: 1234, - }, - } - - try { - await cartService.updateLineItem( - IdMap.getId("cartWithLine"), - IdMap.getId("existingLine"), - lineItem + await expect( + cartService.updateLineItem( + IdMap.getId("cannot"), + IdMap.getId("existing"), + { quantity: 2 } ) - } catch (err) { - expect(err.message).toEqual( - `Inventory doesn't cover the desired quantity` - ) - } + ).rejects.toThrow(`Inventory doesn't cover the desired quantity`) }) }) describe("updateEmail", () => { + const customerService = { + retrieveByEmail: jest.fn().mockImplementation(email => { + if (email === "no@mail.com") { + return Promise.reject() + } + return Promise.resolve({ id: IdMap.getId("existing") }) + }), + create: jest + .fn() + .mockReturnValue(Promise.resolve({ id: IdMap.getId("newCus") })), + withTransaction: function() { + return this + }, + } + const cartRepository = MockRepository({ + findOne: () => Promise.resolve({}), + }) const cartService = new CartService({ - cartModel: CartModelMock, - eventBusService: EventBusServiceMock, - customerService: CustomerServiceMock, + manager: MockManager, + totalsService, + cartRepository, + eventBusService, + customerService, }) beforeEach(() => { @@ -585,50 +734,78 @@ describe("CartService", () => { }) it("successfully updates an email", async () => { - await cartService.updateEmail( - IdMap.getId("emptyCart"), - "test@testdom.com" - ) + await cartService.update(IdMap.getId("emptyCart"), { + email: "test@testDom.com", + }) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(2) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( - "cart.customer_updated", - expect.any(Object) - ) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(2) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("emptyCart"), + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + customer_id: IdMap.getId("existing"), + customer: { + id: IdMap.getId("existing"), }, - { - $set: { - email: "test@testdom.com", - customer_id: IdMap.getId("testdom"), - }, - } + email: "test@testdom.com", + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + }) + }) + + it("creates a new customer", async () => { + await cartService.update(IdMap.getId("emptyCart"), { + email: "no@Mail.com", + }) + + expect(eventBusService.emit).toHaveBeenCalledTimes(2) + expect(eventBusService.emit).toHaveBeenCalledWith( + "cart.updated", + expect.any(Object) ) + + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + customer_id: IdMap.getId("newCus"), + customer: { id: IdMap.getId("newCus") }, + email: "no@mail.com", + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + }) }) it("throws on invalid email", async () => { - try { - await cartService.updateEmail(IdMap.getId("emptyCart"), "test@test") - } catch (err) { - expect(err.message).toEqual("The email is not valid") - } - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(0) + await expect( + cartService.update(IdMap.getId("emptyCart"), { email: "test@test" }) + ).rejects.toThrow("The email is not valid") }) }) describe("updateBillingAddress", () => { + const cartRepository = MockRepository({ + findOne: () => + Promise.resolve({ + region: { countries: [{ iso_2: "us" }] }, + }), + }) + + const addressRepository = MockRepository({ create: c => c }) + const cartService = new CartService({ - cartModel: CartModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + addressRepository, + eventBusService, }) beforeEach(() => { @@ -647,88 +824,91 @@ describe("CartService", () => { phone: "+1 (222) 333 4444", } - await cartService.updateBillingAddress(IdMap.getId("emptyCart"), address) + await cartService.update(IdMap.getId("emptyCart"), { + billing_address: address, + }) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("emptyCart"), - }, - { - $set: { billing_address: address }, - } - ) - }) + expect(addressRepository.create).toHaveBeenCalledTimes(1) + expect(addressRepository.create).toHaveBeenCalledWith({ + ...address, + country_code: "us", + }) - it("throws on invalid address", async () => { - const address = { - last_name: "James", - address_1: "24 Dunks Drive", - city: "Los Angeles", - country_code: "US", - province: "CA", - postal_code: "93011", - } - - try { - await cartService.updateBillingAddress( - IdMap.getId("emptyCart"), - address - ) - } catch (err) { - expect(err.message).toEqual(`"first_name" is required`) - } - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(0) + expect(cartRepository.save).toHaveBeenCalledWith({ + region: { countries: [{ iso_2: "us" }] }, + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + billing_address: address, + }) }) }) describe("updateShippingAddress", () => { + const cartRepository = MockRepository({ + findOne: () => + Promise.resolve({ + region: { countries: [{ iso_2: "us" }] }, + }), + }) + const addressRepository = MockRepository({ create: c => c }) + const cartService = new CartService({ - cartModel: CartModelMock, - eventBusService: EventBusServiceMock, - regionService: RegionServiceMock, + manager: MockManager, + addressRepository, + totalsService, + cartRepository, + eventBusService, }) beforeEach(() => { jest.clearAllMocks() }) - it("successfully updates billing address", async () => { + it("successfully updates shipping address", async () => { const address = { first_name: "LeBron", last_name: "James", address_1: "24 Dunks Drive", city: "Los Angeles", - country_code: "US", + country_code: "us", province: "CA", postal_code: "93011", phone: "+1 (222) 333 4444", } - await cartService.updateShippingAddress(IdMap.getId("emptyCart"), address) + await cartService.update(IdMap.getId("emptyCart"), { + shipping_address: address, + }) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("emptyCart"), - }, - { - $set: { shipping_address: address }, - } - ) + expect(addressRepository.create).toHaveBeenCalledTimes(1) + expect(addressRepository.create).toHaveBeenCalledWith({ + ...address, + }) + + expect(cartRepository.save).toHaveBeenCalledWith({ + region: { countries: [{ iso_2: "us" }] }, + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + shipping_address: address, + }) }) it("throws if country not in region", async () => { @@ -744,37 +924,81 @@ describe("CartService", () => { } await expect( - cartService.updateShippingAddress(IdMap.getId("emptyCart"), address) + cartService.update(IdMap.getId("emptyCart"), { + shipping_address: address, + }) ).rejects.toThrow("Shipping country must be in the cart region") - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(0) - }) - - it("throws on invalid address", async () => { - const address = { - // Missing first_name - last_name: "James", - address_1: "24 Dunks Drive", - city: "Los Angeles", - country_code: "US", - province: "CA", - postal_code: "93011", - } - - await expect( - cartService.updateShippingAddress(IdMap.getId("emptyCart"), address) - ).rejects.toThrow(`"first_name" is required`) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(0) }) }) describe("setRegion", () => { + const lineItemService = { + update: jest.fn(r => r), + delete: jest.fn(), + withTransaction: function() { + return this + }, + } + const addressRepository = MockRepository({ create: c => c }) + const productVariantService = { + getRegionPrice: jest.fn().mockImplementation(id => { + if (id === IdMap.getId("fail")) { + return Promise.reject() + } + return Promise.resolve(100) + }), + } + const regionService = { + retrieve: jest.fn().mockReturnValue( + Promise.resolve({ + id: "region", + countries: [{ iso_2: "us" }], + }) + ), + } + const cartRepository = MockRepository({ + findOne: () => + Promise.resolve({ + items: [ + { + id: IdMap.getId("testitem"), + }, + { + id: IdMap.getId("fail"), + variant_id: IdMap.getId("fail"), + }, + ], + payment_sessions: [{ id: IdMap.getId("removes") }], + discounts: [ + { + id: IdMap.getId("stays"), + regions: [{ id: IdMap.getId("region-us") }], + }, + { + id: IdMap.getId("removes"), + regions: [], + }, + ], + }), + }) + const paymentProviderService = { + deleteSession: jest.fn(), + updateSession: jest.fn(), + createSession: jest.fn(), + withTransaction: function() { + return this + }, + } const cartService = new CartService({ - cartModel: CartModelMock, - regionService: RegionServiceMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + paymentProviderService, + addressRepository, + totalsService, + cartRepository, + regionService, + lineItemService, + productVariantService, + eventBusService, }) beforeEach(() => { @@ -782,142 +1006,83 @@ describe("CartService", () => { }) it("successfully set new region", async () => { - await cartService.setRegion( - IdMap.getId("fr-cart"), - IdMap.getId("region-us") - ) + await cartService.update(IdMap.getId("fr-cart"), { + region_id: IdMap.getId("region-us"), + }) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( + expect(lineItemService.delete).toHaveBeenCalledTimes(1) + expect(lineItemService.delete).toHaveBeenCalledWith(IdMap.getId("fail")) + + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith( + IdMap.getId("testitem"), { - _id: IdMap.getId("fr-cart"), - }, - { - $set: { - region_id: IdMap.getId("region-us"), - shipping_methods: [], - shipping_address: { - country_code: "US", - }, - items: [ - { - _id: IdMap.getId("line"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - has_shipping: false, - content: [ - { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product1"), - }, - quantity: 1, - }, - { - unit_price: 12, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product1"), - }, - quantity: 1, - }, - ], - quantity: 10, - }, - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - has_shipping: false, - content: { - unit_price: 12, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product2"), - }, - quantity: 1, - }, - quantity: 10, - }, - ], - }, + unit_price: 100, + has_shipping: false, } ) - }) - it("successfully set new region", async () => { - await cartService.setRegion( - IdMap.getId("complete-cart"), - IdMap.getId("region-us") - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("complete-cart"), + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + region_id: "region", + region: { + id: "region", + countries: [{ iso_2: "us" }], }, - { - $set: { - items: [], - region_id: IdMap.getId("region-us"), - shipping_methods: [], - payment_sessions: [], - payment_method: undefined, - shipping_address: { - first_name: "hi", - last_name: "you", - country_code: "US", - city: "of lights", - address_1: "You bet street", - postal_code: "4242", - }, + items: [IdMap.getId("testitem"), null], + payment_session: null, + payment_sessions: [], + gift_cards: [], + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + discounts: [ + { + id: IdMap.getId("stays"), + regions: [{ id: IdMap.getId("region-us") }], }, - } - ) - }) - - it("filters items that don't have region prices", async () => { - await cartService.setRegion( - IdMap.getId("cartWithLine"), - IdMap.getId("testRegion") - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), - }, - { - $set: { - region_id: IdMap.getId("testRegion"), - items: [], - }, - } - ) + ], + }) }) }) - describe("setPaymentMethod", () => { + describe("setPaymentSession", () => { + const cartRepository = MockRepository({ + findOne: () => { + return Promise.resolve({ + region: { + payment_providers: [ + { + id: "test-provider", + }, + ], + }, + payment_sessions: [ + { + id: IdMap.getId("test-session"), + provider_id: "test-provider", + }, + ], + }) + }, + }) + + const paymentSessionRepository = MockRepository({}) + const cartService = new CartService({ - cartModel: CartModelMock, - regionService: RegionServiceMock, - paymentProviderService: PaymentProviderServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + paymentSessionRepository, + totalsService, + cartRepository, + eventBusService, }) beforeEach(() => { @@ -925,73 +1090,113 @@ describe("CartService", () => { }) it("successfully sets a payment method", async () => { - const paymentMethod = { - provider_id: "default_provider", - data: { - money_id: "success", - }, - } - - await cartService.setPaymentMethod( + await cartService.setPaymentSession( IdMap.getId("cartWithLine"), - paymentMethod + "test-provider" ) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - - expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(RegionServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("testRegion") - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), - }, - { - $set: { payment_method: paymentMethod }, - } - ) + expect(paymentSessionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-session"), + provider_id: "test-provider", + is_selected: true, + }) }) it("fails if the region does not contain the provider_id", async () => { - const paymentMethod = { - provider_id: "unknown_provider", - data: { - money_id: "success", - }, - } - - try { - await cartService.setPaymentMethod( - IdMap.getId("cartWithLine"), - paymentMethod - ) - } catch (err) { - expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(RegionServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("testRegion") - ) - - expect(err.message).toEqual( - `The payment method is not available in this region` - ) - } + await expect( + cartService.setPaymentSession(IdMap.getId("cartWithLine"), "unknown") + ).rejects.toThrow(`The payment method is not available in this region`) }) }) describe("setPaymentSessions", () => { + const cart1 = { + total: 100, + payment_sessions: [], + region: { + payment_providers: [{ id: "provider_1" }, { id: "provider_2" }], + }, + } + + const cart2 = { + total: 100, + payment_sessions: [{ provider_id: "provider_1" }], + region: { + payment_providers: [{ id: "provider_1" }, { id: "provider_2" }], + }, + } + + const cart3 = { + total: 100, + payment_sessions: [ + { provider_id: "provider_1" }, + { provider_id: "not_in_region" }, + ], + region: { + payment_providers: [{ id: "provider_1" }, { id: "provider_2" }], + }, + } + + const cart4 = { + total: 0, + payment_sessions: [ + { provider_id: "provider_1" }, + { provider_id: "provider_2" }, + ], + region: { + payment_providers: [{ id: "provider_1" }, { id: "provider_2" }], + }, + } + + const cart5 = { + total: -1, + payment_sessions: [ + { provider_id: "provider_1" }, + { provider_id: "provider_2" }, + ], + region: { + payment_providers: [{ id: "provider_1" }, { id: "provider_2" }], + }, + } + + const cartRepository = MockRepository({ + findOne: q => { + if (q.where.id === IdMap.getId("cart-to-filter")) { + return Promise.resolve(cart3) + } + if (q.where.id === IdMap.getId("cart-with-session")) { + return Promise.resolve(cart2) + } + if (q.where.id === IdMap.getId("cart-remove")) { + return Promise.resolve(cart4) + } + if (q.where.id === IdMap.getId("cart-negative")) { + return Promise.resolve(cart4) + } + return Promise.resolve(cart1) + }, + }) + + const paymentProviderService = { + deleteSession: jest.fn(), + updateSession: jest.fn(), + createSession: jest.fn(), + withTransaction: function() { + return this + }, + } + const cartService = new CartService({ - cartModel: CartModelMock, - regionService: RegionServiceMock, - paymentProviderService: PaymentProviderServiceMock, - totalsService: TotalsServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + totalsService, + cartRepository, + paymentProviderService, + eventBusService, }) beforeEach(() => { @@ -1001,544 +1206,454 @@ describe("CartService", () => { it("initializes payment sessions for each of the providers", async () => { await cartService.setPaymentSessions(IdMap.getId("cartWithLine")) - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( - "cart.updated", - expect.any(Object) + expect(paymentProviderService.createSession).toHaveBeenCalledTimes(2) + expect(paymentProviderService.createSession).toHaveBeenCalledWith( + "provider_1", + cart1 ) - - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(2) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledWith( - "default_provider", - carts.cartWithLine - ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledWith( - "unregistered", - carts.cartWithLine - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithLine"), - }, - { - $set: { - payment_sessions: [ - { - provider_id: "default_provider", - data: { - id: "default_provider_session", - cartId: IdMap.getId("cartWithLine"), - }, - }, - { - provider_id: "unregistered", - data: { - id: "unregistered_session", - cartId: IdMap.getId("cartWithLine"), - }, - }, - ], - }, - } - ) - }) - - it("updates payment sessions for existing sessions", async () => { - await cartService.setPaymentSessions(IdMap.getId("cartWithPaySessions")) - - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0) - - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(2) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledWith( - { - provider_id: "default_provider", - data: { - id: "default_provider_session", - }, - }, - carts.cartWithPaySessions - ) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledWith( - { - provider_id: "unregistered", - data: { - id: "unregistered_session", - }, - }, - carts.cartWithPaySessions - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithPaySessions"), - }, - { - $set: { - payment_sessions: [ - { - provider_id: "default_provider", - data: { - id: "default_provider_session_updated", - }, - }, - { - provider_id: "unregistered", - data: { - id: "unregistered_session_updated", - }, - }, - ], - }, - } + expect(paymentProviderService.createSession).toHaveBeenCalledWith( + "provider_2", + cart1 ) }) it("filters sessions not available in the region", async () => { - await cartService.setPaymentSessions( - IdMap.getId("cartWithPaySessionsDifRegion") - ) + await cartService.setPaymentSessions(IdMap.getId("cart-to-filter")) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1) - - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(1) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledWith( - { - provider_id: "default_provider", - data: { - id: "default_provider_session", - }, - }, - carts.cartWithPaySessionsDifRegion - ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledWith( - "france-provider", - carts.cartWithPaySessionsDifRegion - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithPaySessionsDifRegion"), - }, - { - $set: { - payment_sessions: [ - { - provider_id: "default_provider", - data: { - id: "default_provider_session_updated", - }, - }, - { - provider_id: "france-provider", - data: { - id: "france-provider_session", - cartId: IdMap.getId("cartWithPaySessionsDifRegion"), - }, - }, - ], - }, - } - ) - }) - }) - - describe("retrievePaymentSession", () => { - const cartService = new CartService({ - cartModel: CartModelMock, - eventBusService: EventBusServiceMock, - }) - - let res - - describe("it retrieves the correct payment session", () => { - beforeAll(async () => { - jest.clearAllMocks() - res = await cartService.retrievePaymentSession( - IdMap.getId("cartWithPaySessions"), - "default_provider" - ) - }) - - it("retrieves the cart", () => { - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("cartWithPaySessions"), - }) - }) - - it("finds the correct payment session", () => { - expect(res.provider_id).toEqual("default_provider") - expect(res.data).toEqual({ - id: "default_provider_session", - }) + expect(paymentProviderService.createSession).toHaveBeenCalledTimes(1) + expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(1) + expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(1) + expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({ + provider_id: "not_in_region", }) }) - describe("it fails when provider doesn't match open session", () => { - beforeAll(async () => { - jest.clearAllMocks() - try { - await cartService.retrievePaymentSession( - IdMap.getId("cartWithPaySessions"), - "nono" - ) - } catch (err) { - res = err - } - }) + it("removes if cart total === 0", async () => { + await cartService.setPaymentSessions(IdMap.getId("cart-remove")) - it("retrieves the cart", () => { - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("cartWithPaySessions"), - }) + expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(0) + expect(paymentProviderService.createSession).toHaveBeenCalledTimes(0) + expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(2) + expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({ + provider_id: "provider_1", }) + expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({ + provider_id: "provider_2", + }) + }) - it("throws invalid data errro", () => { - expect(res.message).toEqual( - "The provider_id did not match any open payment sessions" - ) + it("removes if cart total < 0", async () => { + await cartService.setPaymentSessions(IdMap.getId("cart-negative")) + + expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(0) + expect(paymentProviderService.createSession).toHaveBeenCalledTimes(0) + expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(2) + expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({ + provider_id: "provider_1", + }) + expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({ + provider_id: "provider_2", }) }) }) describe("addShippingMethod", () => { - const cartService = new CartService({ - cartModel: CartModelMock, - shippingProfileService: ShippingProfileServiceMock, - shippingOptionService: ShippingOptionServiceMock, - eventBusService: EventBusServiceMock, + const buildCart = (id, config = {}) => { + return { + id: IdMap.getId(id), + items: (config.items || []).map(i => ({ + id: IdMap.getId(i.id), + variant: { + product: { + profile_id: IdMap.getId(i.profile), + }, + }, + })), + shipping_methods: (config.shipping_methods || []).map(m => ({ + id: IdMap.getId(m.id), + shipping_option: { + profile_id: IdMap.getId(m.profile), + }, + })), + } + } + + const cart1 = buildCart("cart") + const cart2 = buildCart("existing", { + shipping_methods: [{ id: "ship1", profile: "profile1" }], + }) + const cart3 = buildCart("lines", { + items: [{ id: "line", profile: "profile1" }], }) - describe("successfully adds the shipping method", () => { + const cartRepository = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("lines"): + return Promise.resolve(cart3) + case IdMap.getId("existing"): + return Promise.resolve(cart2) + default: + return Promise.resolve(cart1) + } + }, + }) + + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const shippingOptionService = { + createShippingMethod: jest.fn().mockImplementation(id => { + return Promise.resolve({ + shipping_option: { + profile_id: id, + }, + }) + }), + deleteShippingMethod: jest.fn(), + withTransaction: function() { + return this + }, + } + + const cartService = new CartService({ + manager: MockManager, + totalsService, + cartRepository, + shippingOptionService, + lineItemService, + eventBusService, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("successfully adds the shipping method", async () => { const data = { - id: "testshipperid", + id: "test", extra: "yes", } - beforeAll(async () => { - jest.clearAllMocks() - const cartId = IdMap.getId("cartWithPaySessions") - await cartService.addShippingMethod( - cartId, - IdMap.getId("freeShipping"), - data - ) - }) - - it("validates option", () => { - expect( - ShippingOptionServiceMock.validateCartOption - ).toHaveBeenCalledWith( - IdMap.getId("freeShipping"), - carts.cartWithPaySessions - ) - }) - - it("validates fulfillment data", () => { - expect( - ShippingOptionServiceMock.validateFulfillmentData - ).toHaveBeenCalledWith( - IdMap.getId("freeShipping"), - data, - carts.cartWithPaySessions - ) - }) - - it("gets shipping profile", () => { - expect(ShippingProfileServiceMock.list).toHaveBeenCalledWith({ - shipping_options: IdMap.getId("freeShipping"), - }) - }) - - it("updates cart", () => { - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( - "cart.updated", - expect.any(Object) - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("cartWithPaySessions"), - }, - { - $set: { - items: [ - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - has_shipping: true, - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - quantity: 10, - }, - ], - shipping_methods: [ - { - _id: IdMap.getId("freeShipping"), - price: 0, - provider_id: "default_provider", - profile_id: IdMap.getId("default_profile"), - data, - }, - ], - }, - } - ) - }) + await cartService.addShippingMethod( + IdMap.getId("cart"), + IdMap.getId("option"), + data + ) + expect( + shippingOptionService.createShippingMethod + ).toHaveBeenCalledWith(IdMap.getId("option"), data, { cart: cart1 }) }) - describe("successfully overrides existing profile shipping method", () => { + it("successfully overrides existing profile shipping method", async () => { const data = { id: "testshipperid", } - - beforeAll(async () => { - jest.clearAllMocks() - const cartId = IdMap.getId("fr-cart") - await cartService.addShippingMethod( - cartId, - IdMap.getId("freeShipping"), - data - ) - }) - - it("updates cart", () => { - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("fr-cart"), - }, - { - $set: { - items: carts.frCart.items.map(i => ({ - ...i, - has_shipping: false, - })), - shipping_methods: [ - { - _id: IdMap.getId("freeShipping"), - price: 0, - provider_id: "default_provider", - profile_id: IdMap.getId("default_profile"), - data: { - id: "testshipperid", - }, - }, - ], - }, - } - ) + await cartService.addShippingMethod( + IdMap.getId("existing"), + IdMap.getId("profile1"), + data + ) + expect( + shippingOptionService.createShippingMethod + ).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart2 }) + expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledWith({ + id: IdMap.getId("ship1"), + shipping_option: { + profile_id: IdMap.getId("profile1"), + }, }) }) - describe("successfully adds additional shipping method", () => { + it("successfully adds additional shipping method", async () => { const data = { id: "additional_shipper_id", } - beforeAll(async () => { - jest.clearAllMocks() - const cartId = IdMap.getId("fr-cart") - await cartService.addShippingMethod( - cartId, - IdMap.getId("additional"), - data - ) - }) + await cartService.addShippingMethod( + IdMap.getId("existing"), + IdMap.getId("additional"), + data + ) - it("updates cart", () => { - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("fr-cart"), - }, - { - $set: { - items: carts.frCart.items.map(i => ({ - ...i, - has_shipping: false, - })), - shipping_methods: [ - { - _id: IdMap.getId("freeShipping"), - profile_id: IdMap.getId("default_profile"), - }, - { - _id: IdMap.getId("additional"), - price: 0, - profile_id: IdMap.getId("additional_profile"), - provider_id: "default_provider", - data, - }, - ], - }, - } - ) - }) + expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledTimes( + 0 + ) + expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes( + 1 + ) + expect( + shippingOptionService.createShippingMethod + ).toHaveBeenCalledWith(IdMap.getId("additional"), data, { cart: cart2 }) }) - describe("throws if no profile", () => { - let res - beforeAll(async () => { - jest.clearAllMocks() - const cartId = IdMap.getId("fr-cart") - try { - await cartService.addShippingMethod(cartId, IdMap.getId("fail"), {}) - } catch (err) { - res = err - } - }) + it("updates item shipping", async () => { + const data = { + id: "shipper", + } - it("throw error", () => { - expect(res.message).toEqual( - "Shipping Method must belong to a shipping profile" - ) + await cartService.addShippingMethod( + IdMap.getId("lines"), + IdMap.getId("profile1"), + data + ) + + expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledTimes( + 0 + ) + expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes( + 1 + ) + expect( + shippingOptionService.createShippingMethod + ).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart3 }) + + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith(IdMap.getId("line"), { + has_shipping: true, }) }) }) describe("applyDiscount", () => { - const cartService = new CartService({ - cartModel: CartModelMock, - discountService: DiscountServiceMock, - eventBusService: EventBusServiceMock, + const cartRepository = MockRepository({ + findOne: q => { + if (q.where.id === IdMap.getId("with-d")) { + return Promise.resolve({ + id: IdMap.getId("cart"), + discounts: [ + { + code: "1234", + rule: { + type: "fixed", + }, + }, + { + code: "FS1234", + rule: { + type: "free_shipping", + }, + }, + ], + region_id: IdMap.getId("good"), + }) + } + return Promise.resolve({ + id: IdMap.getId("cart"), + discounts: [], + region_id: IdMap.getId("good"), + }) + }, }) + + const discountService = { + retrieveByCode: jest.fn().mockImplementation(code => { + if (code === "US10") { + return Promise.resolve({ + regions: [{ id: IdMap.getId("bad") }], + }) + } + if (code === "FREESHIPPING") { + return Promise.resolve({ + id: IdMap.getId("freeship"), + code: "FREESHIPPING", + regions: [{ id: IdMap.getId("good") }], + rule: { + type: "free_shipping", + }, + }) + } + return Promise.resolve({ + id: IdMap.getId("10off"), + code: "10%OFF", + regions: [{ id: IdMap.getId("good") }], + rule: { + type: "percentage", + }, + }) + }), + } + + const cartService = new CartService({ + manager: MockManager, + totalsService, + cartRepository, + discountService, + eventBusService, + }) + beforeEach(async () => { jest.clearAllMocks() }) it("successfully applies discount to cart", async () => { - await cartService.applyDiscount(IdMap.getId("fr-cart"), "10%OFF") - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("fr-cart"), + await cartService.update(IdMap.getId("fr-cart"), { + discounts: [ + { + code: "10%OFF", + }, + ], }) - - expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) - expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( "cart.updated", expect.any(Object) ) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledWith("10%OFF") - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("fr-cart"), - }, - { - $push: { discounts: discounts.total10Percent }, - } - ) + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("cart"), + region_id: IdMap.getId("good"), + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + discounts: [ + { + id: IdMap.getId("10off"), + code: "10%OFF", + regions: [{ id: IdMap.getId("good") }], + rule: { + type: "percentage", + }, + }, + ], + }) }) it("successfully applies discount to cart and removes old one", async () => { - await cartService.applyDiscount( - IdMap.getId("discount-cart-with-existing"), - "10%OFF" - ) - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("discount-cart-with-existing"), + await cartService.update(IdMap.getId("with-d"), { + discounts: [{ code: "10%OFF" }], }) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledWith("10%OFF") - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("discount-cart-with-existing"), - }, - { - $push: { discounts: discounts.total10Percent }, - $pull: { discounts: { _id: IdMap.getId("item10Percent") } }, - } - ) + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("cart"), + region_id: IdMap.getId("good"), + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + discounts: [ + { + id: IdMap.getId("10off"), + code: "10%OFF", + regions: [{ id: IdMap.getId("good") }], + rule: { + type: "percentage", + }, + }, + ], + }) }) it("successfully applies free shipping", async () => { - await cartService.applyDiscount( - IdMap.getId("discount-cart-with-existing"), - "FREESHIPPING" - ) - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("discount-cart-with-existing"), + await cartService.update(IdMap.getId("with-d"), { + discounts: [{ code: "10%OFF" }, { code: "FREESHIPPING" }], }) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledWith( - "FREESHIPPING" - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("discount-cart-with-existing"), - }, - { - $push: { discounts: discounts.freeShipping }, - } - ) - }) - - it("successfully resolves ", async () => { - await cartService.applyDiscount( - IdMap.getId("discount-cart-with-existing"), - "FREESHIPPING" - ) - expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("discount-cart-with-existing"), + expect(discountService.retrieveByCode).toHaveBeenCalledTimes(2) + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("cart"), + discounts: [ + { + id: IdMap.getId("10off"), + code: "10%OFF", + regions: [{ id: IdMap.getId("good") }], + rule: { + type: "percentage", + }, + }, + { + id: IdMap.getId("freeship"), + code: "FREESHIPPING", + regions: [{ id: IdMap.getId("good") }], + rule: { + type: "free_shipping", + }, + }, + ], + discount_total: 0, + shipping_total: 0, + subtotal: 0, + tax_total: 0, + total: 0, + region_id: IdMap.getId("good"), }) - - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledTimes(1) - expect(DiscountServiceMock.retrieveByCode).toHaveBeenCalledWith( - "FREESHIPPING" - ) - - expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(CartModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("discount-cart-with-existing"), - }, - { - $push: { discounts: discounts.freeShipping }, - } - ) }) it("throws if discount is not available in region", async () => { - try { - await cartService.applyDiscount( - IdMap.getId("discount-cart-with-existing"), - "US10" - ) - } catch (error) { - expect(error.message).toEqual( - "The discount is not available in current region" - ) - } + await expect( + cartService.update(IdMap.getId("cart"), { + discounts: [{ code: "US10" }], + }) + ).rejects.toThrow("The discount is not available in current region") + }) + }) + + describe("removeDiscount", () => { + const cartRepository = MockRepository({ + findOne: q => { + return Promise.resolve({ + id: IdMap.getId("cart"), + discounts: [ + { + code: "1234", + rule: { + type: "fixed", + }, + }, + { + code: "FS1234", + rule: { + type: "free_shipping", + }, + }, + ], + region_id: IdMap.getId("good"), + }) + }, + }) + + const cartService = new CartService({ + manager: MockManager, + totalsService, + cartRepository, + eventBusService, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully removes discount", async () => { + await cartService.removeDiscount(IdMap.getId("fr-cart"), "1234") + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "cart.updated", + expect.any(Object) + ) + + expect(cartRepository.save).toHaveBeenCalledTimes(1) + expect(cartRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("cart"), + region_id: IdMap.getId("good"), + discounts: [ + { + code: "FS1234", + rule: { + type: "free_shipping", + }, + }, + ], + }) }) }) }) diff --git a/packages/medusa/src/services/__tests__/customer.js b/packages/medusa/src/services/__tests__/customer.js index 407b43ad15..524c665e40 100644 --- a/packages/medusa/src/services/__tests__/customer.js +++ b/packages/medusa/src/services/__tests__/customer.js @@ -1,147 +1,141 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import { add } from "winston" import CustomerService from "../customer" -import { CustomerModelMock, customers } from "../../models/__mocks__/customer" + +const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, +} describe("CustomerService", () => { describe("retrieve", () => { - let result - beforeAll(async () => { + const customerRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + }) + const customerService = new CustomerService({ + manager: MockManager, + customerRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() - const customerService = new CustomerService({ - customerModel: CustomerModelMock, - }) - result = await customerService.retrieve(IdMap.getId("testCustomer")) }) - it("calls customer model functions", () => { - expect(CustomerModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CustomerModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("testCustomer"), - }) - }) + it("successfully retrieves a customer", async () => { + const result = await customerService.retrieve(IdMap.getId("ironman")) - it("returns the customer", () => { - expect(result).toEqual(customers.testCustomer) + expect(customerRepository.findOne).toHaveBeenCalledTimes(1) + expect(customerRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("ironman") }, + }) + + expect(result.id).toEqual(IdMap.getId("ironman")) }) }) describe("retrieveByEmail", () => { - let result - beforeAll(async () => { + const customerRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + }) + const customerService = new CustomerService({ + manager: MockManager, + customerRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() - const customerService = new CustomerService({ - customerModel: CustomerModelMock, - }) - result = await customerService.retrieveByEmail("oliver@medusa.com") }) - it("calls customer model functions", () => { - expect(CustomerModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CustomerModelMock.findOne).toHaveBeenCalledWith({ - email: "oliver@medusa.com", - }) - }) + it("successfully retrieves a customer by email", async () => { + const result = await customerService.retrieveByEmail("tony@stark.com") - it("returns the customer", () => { - expect(result).toEqual(customers.testCustomer) + expect(customerRepository.findOne).toHaveBeenCalledTimes(1) + expect(customerRepository.findOne).toHaveBeenCalledWith({ + where: { email: "tony@stark.com" }, + }) + + expect(result.id).toEqual(IdMap.getId("ironman")) }) }) describe("retrieveByPhone", () => { - let result - beforeAll(async () => { - jest.clearAllMocks() - const customerService = new CustomerService({ - customerModel: CustomerModelMock, - }) - result = await customerService.retrieveByPhone("12345678") + const customerRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), }) - - it("calls customer model functions", () => { - expect(CustomerModelMock.findOne).toHaveBeenCalledTimes(1) - expect(CustomerModelMock.findOne).toHaveBeenCalledWith({ - phone: "12345678", - }) - }) - - it("returns the customer", () => { - expect(result).toEqual(customers.customerWithPhone) - }) - }) - - describe("setMetadata", () => { const customerService = new CustomerService({ - customerModel: CustomerModelMock, + manager: MockManager, + customerRepository, }) - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks() }) - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await customerService.setMetadata(`${id}`, "metadata", "testMetadata") + it("successfully retrieves a customer by email", async () => { + const result = await customerService.retrieveByPhone("12341234") - expect(CustomerModelMock.updateOne).toBeCalledTimes(1) - expect(CustomerModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } - ) - }) + expect(customerRepository.findOne).toHaveBeenCalledTimes(1) + expect(customerRepository.findOne).toHaveBeenCalledWith({ + where: { phone: "12341234" }, + }) - it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() - - try { - await customerService.setMetadata(`${id}`, 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "Key type is invalid. Metadata keys must be strings" - ) - } + expect(result.id).toEqual(IdMap.getId("ironman")) }) }) describe("create", () => { - const customerService = new CustomerService({ - customerModel: CustomerModelMock, + const customerRepository = MockRepository({ + findOne: query => { + if (query.where.email === "tony@stark.com") { + return Promise.resolve({ + id: IdMap.getId("exists"), + password_hash: "test", + }) + } + return Promise.resolve({ id: IdMap.getId("ironman") }) + }, }) - beforeEach(() => { + const customerService = new CustomerService({ + manager: MockManager, + customerRepository, + eventBusService, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("calls model layer create", async () => { + it("successfully create a customer", async () => { await customerService.create({ email: "oliver@medusa.com", first_name: "Oliver", last_name: "Juhl", }) - expect(CustomerModelMock.create).toBeCalledTimes(1) - expect(CustomerModelMock.create).toBeCalledWith({ + expect(customerRepository.create).toBeCalledTimes(1) + expect(customerRepository.create).toBeCalledWith({ email: "oliver@medusa.com", first_name: "Oliver", last_name: "Juhl", }) }) - it("calls model layer create", async () => { + it("successfully updates an existing customer on create", async () => { await customerService.create({ - email: "new@medusa.com", - first_name: "Oliver", - last_name: "Juhl", - password: "secretsauce", + email: "tony@stark.com", + password: "stark123", + has_account: false, }) - expect(CustomerModelMock.create).toBeCalledTimes(1) - expect(CustomerModelMock.create).toBeCalledWith({ - email: "new@medusa.com", - first_name: "Oliver", - last_name: "Juhl", + expect(customerRepository.save).toBeCalledTimes(1) + expect(customerRepository.save).toBeCalledWith({ + id: IdMap.getId("exists"), + email: "tony@stark.com", + password_hash: expect.anything(), has_account: true, - password_hash: expect.stringMatching(/^.{128}$/), }) }) @@ -149,8 +143,6 @@ describe("CustomerService", () => { expect( customerService.create({ email: "olivermedusa.com", - first_name: "Oliver", - last_name: "Juhl", }) ).rejects.toThrow("The email is not valid") }) @@ -170,47 +162,54 @@ describe("CustomerService", () => { }) describe("update", () => { - const customerService = new CustomerService({ - customerModel: CustomerModelMock, + const customerRepository = MockRepository({ + findOne: query => { + return Promise.resolve({ id: IdMap.getId("ironman") }) + }, }) - beforeEach(() => { + const customerService = new CustomerService({ + manager: MockManager, + customerRepository, + eventBusService, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully updates a customer", async () => { - await customerService.update(IdMap.getId("testCustomer"), { + await customerService.update(IdMap.getId("ironman"), { first_name: "Olli", last_name: "Test", - email: "oliver@medusa2.com", }) - expect(CustomerModelMock.updateOne).toBeCalledTimes(1) - expect(CustomerModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("testCustomer") }, - { - $set: { - first_name: "Olli", - last_name: "Test", - email: "oliver@medusa2.com", - }, + expect(customerRepository.save).toBeCalledTimes(1) + expect(customerRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + first_name: "Olli", + last_name: "Test", + }) + }) + + it("successfully updates customer metadata", async () => { + await customerService.update(IdMap.getId("ironman"), { + metadata: { + some: "test", }, - { runValidators: true } - ) + }) + + expect(customerRepository.save).toBeCalledTimes(1) + expect(customerRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + metadata: { + some: "test", + }, + }) }) - it("fails if metadata updates are attempted", async () => { - try { - await customerService.update(IdMap.getId("testCustomer"), { - metadata: "Nononono", - }) - } catch (err) { - expect(err.message).toEqual("Use setMetadata to update metadata fields") - } - }) - - it("updates with billing address", async () => { - await customerService.update(IdMap.getId("testCustomer"), { + it("successfully updates with billing address", async () => { + await customerService.update(IdMap.getId("ironman"), { first_name: "Olli", last_name: "Test", billing_address: { @@ -224,62 +223,141 @@ describe("CustomerService", () => { }, }) - expect(CustomerModelMock.updateOne).toBeCalledTimes(1) - expect(CustomerModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("testCustomer") }, - { - $set: { - first_name: "Olli", - last_name: "Test", - billing_address: { - first_name: "Olli", - last_name: "Juhl", - address_1: "Laksegade", - city: "Copenhagen", - country_code: "DK", - postal_code: "2100", - phone: "+1 (222) 333 4444", - }, - }, + expect(customerRepository.save).toBeCalledTimes(1) + expect(customerRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + first_name: "Olli", + last_name: "Test", + billing_address: { + first_name: "Olli", + last_name: "Juhl", + address_1: "Laksegade", + city: "Copenhagen", + country_code: "DK", + postal_code: "2100", + phone: "+1 (222) 333 4444", }, - { runValidators: true } - ) + }) + }) + }) + + describe("updateAddress", () => { + const addressRepository = MockRepository({ + findOne: query => { + return Promise.resolve({ + id: IdMap.getId("hollywood-boulevard"), + address_1: "Hollywood Boulevard 2", + }) + }, }) - it("updates with password", async () => { - await customerService.update(IdMap.getId("testCustomer"), { - password: "newpassword", - }) + const customerService = new CustomerService({ + manager: MockManager, + addressRepository, + }) - expect(CustomerModelMock.updateOne).toBeCalledTimes(1) - expect(CustomerModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("testCustomer") }, + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully updates address", async () => { + await customerService.updateAddress( + IdMap.getId("ironman"), + IdMap.getId("hollywood-boulevard"), { - $set: { - has_account: true, - password_hash: expect.stringMatching(/^.{128}$/), - }, - }, - { runValidators: true } + first_name: "Tony", + last_name: "Stark", + address_1: "Hollywood Boulevard 1", + city: "Los Angeles", + country_code: "US", + postal_code: "90046", + phone: "+1 (222) 333 4444", + } ) + + expect(addressRepository.save).toBeCalledTimes(1) + expect(addressRepository.save).toBeCalledWith({ + id: IdMap.getId("hollywood-boulevard"), + first_name: "Tony", + last_name: "Stark", + address_1: "Hollywood Boulevard 1", + city: "Los Angeles", + country_code: "US", + postal_code: "90046", + phone: "+1 (222) 333 4444", + }) + }) + + it("throws on invalid address", async () => { + expect( + customerService.updateAddress( + IdMap.getId("ironman"), + IdMap.getId("hollywood-boulevard"), + { + first_name: "Tony", + last_name: "Stark", + address_1: "Hollywood", + } + ) + ).rejects.toThrow("The address is not valid") + }) + }) + + describe("removeAddress", () => { + const addressRepository = MockRepository({ + findOne: query => { + return Promise.resolve({ + id: IdMap.getId("hollywood-boulevard"), + address_1: "Hollywood Boulevard 2", + }) + }, + }) + + const customerService = new CustomerService({ + manager: MockManager, + addressRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully deletes address", async () => { + await customerService.removeAddress( + IdMap.getId("ironman"), + IdMap.getId("hollywood-boulevard") + ) + + expect(addressRepository.softRemove).toBeCalledTimes(1) + expect(addressRepository.softRemove).toBeCalledWith({ + id: IdMap.getId("hollywood-boulevard"), + address_1: "Hollywood Boulevard 2", + }) }) }) describe("delete", () => { - const customerService = new CustomerService({ - customerModel: CustomerModelMock, + const customerRepository = MockRepository({ + findOne: query => { + return Promise.resolve({ id: IdMap.getId("ironman") }) + }, }) - beforeEach(() => { + const customerService = new CustomerService({ + manager: MockManager, + customerRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("deletes customer successfully", async () => { - await customerService.delete(IdMap.getId("deleteId")) + it("successfully deletes customer", async () => { + await customerService.delete(IdMap.getId("ironman")) - expect(CustomerModelMock.deleteOne).toBeCalledTimes(1) - expect(CustomerModelMock.deleteOne).toBeCalledWith({ - _id: IdMap.getId("deleteId"), + expect(customerRepository.softRemove).toBeCalledTimes(1) + expect(customerRepository.softRemove).toBeCalledWith({ + id: IdMap.getId("ironman"), }) }) }) diff --git a/packages/medusa/src/services/__tests__/discount.js b/packages/medusa/src/services/__tests__/discount.js index 9e30a7ae4d..0ec55a1f92 100644 --- a/packages/medusa/src/services/__tests__/discount.js +++ b/packages/medusa/src/services/__tests__/discount.js @@ -1,307 +1,471 @@ import DiscountService from "../discount" -import { DiscountModelMock, discounts } from "../../models/__mocks__/discount" -import { - DynamicDiscountCodeModelMock, - dynamicDiscounts, -} from "../../models/__mocks__/dynamic-discount-code" -import { IdMap } from "medusa-test-utils" -import { ProductVariantServiceMock } from "../__mocks__/product-variant" -import { EventBusServiceMock } from "../__mocks__/event-bus" -import { RegionServiceMock } from "../__mocks__/region" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" describe("DiscountService", () => { describe("create", () => { + const discountRepository = MockRepository({}) + + const discountRuleRepository = MockRepository({}) + + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("france"), + } + }, + } + const discountService = new DiscountService({ - discountModel: DiscountModelMock, + manager: MockManager, + discountRepository, + discountRuleRepository, + regionService, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls model layer create and normalizes code", async () => { + it("successfully creates discount", async () => { await discountService.create({ code: "test", - discount_rule: { + rule: { type: "percentage", allocation: "total", value: 20, }, - regions: [IdMap.getId("fr-cart")], + regions: [IdMap.getId("france")], }) - expect(DiscountModelMock.create).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.create).toHaveBeenCalledWith({ - code: "TEST", - discount_rule: { - type: "percentage", - allocation: "total", - value: 20, - }, - regions: [IdMap.getId("fr-cart")], + expect(discountRuleRepository.create).toHaveBeenCalledTimes(1) + expect(discountRuleRepository.create).toHaveBeenCalledWith({ + type: "percentage", + allocation: "total", + value: 20, }) + + expect(discountRuleRepository.save).toHaveBeenCalledTimes(1) + + expect(discountRepository.create).toHaveBeenCalledTimes(1) + expect(discountRepository.create).toHaveBeenCalledWith({ + code: "TEST", + rule: expect.anything(), + regions: [{ id: IdMap.getId("france") }], + }) + + expect(discountRepository.save).toHaveBeenCalledTimes(1) }) }) describe("retrieve", () => { - let res + const discountRepository = MockRepository({ + findOne: query => { + if (query.where.id) { + return Promise.resolve({ id: IdMap.getId("total10") }) + } + return Promise.resolve(undefined) + }, + }) + const discountService = new DiscountService({ - discountModel: DiscountModelMock, + manager: MockManager, + discountRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls model layer findOne", async () => { - res = await discountService.retrieve(IdMap.getId("total10")) - expect(DiscountModelMock.findOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("total10"), - }) - }) - - it("successfully returns cart", () => { - expect(res).toEqual(discounts.total10Percent) - }) - }) - - describe("retrieveByCode", () => { - let res - const discountService = new DiscountService({ - discountModel: DiscountModelMock, - dynamicDiscountCodeModel: DynamicDiscountCodeModelMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", async () => { - res = await discountService.retrieveByCode("10%off") - expect(DiscountModelMock.findOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.findOne).toHaveBeenCalledWith({ - code: "10%OFF", - }) - expect(res).toEqual(discounts.total10Percent) - }) - - it("finds dynamic code", async () => { - res = await discountService.retrieveByCode("dynamicoff") - expect(DiscountModelMock.findOne).toHaveBeenCalledTimes(2) - expect(DiscountModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("dynamic"), - }) - expect(DiscountModelMock.findOne).toHaveBeenCalledWith({ - code: "DYNAMICOFF", - }) - expect(DynamicDiscountCodeModelMock.findOne).toHaveBeenCalledTimes(1) - expect(DynamicDiscountCodeModelMock.findOne).toHaveBeenCalledWith({ - code: "DYNAMICOFF", - }) - expect(res).toEqual({ - ...discounts.dynamic, - code: "DYNAMICOFF", - }) - }) - }) - - describe("update", () => { - const discountService = new DiscountService({ - discountModel: DiscountModelMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("calls model layer updateOne", async () => { - await discountService.update(IdMap.getId("total10"), { - code: "test", - }) - expect(DiscountModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("total10") }, - { - $set: { code: "test" }, + it("successfully retrieves discount", async () => { + await discountService.retrieve(IdMap.getId("total10")) + expect(discountRepository.findOne).toHaveBeenCalledTimes(1) + expect(discountRepository.findOne).toHaveBeenCalledWith({ + where: { + id: IdMap.getId("total10"), }, - { runValidators: true } - ) - }) - - it("successfully calls model layer with discount_rule", async () => { - await discountService.update(IdMap.getId("total10"), { - discount_rule: { type: "fixed", value: 10, allocation: "total" }, }) - expect(DiscountModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("total10"), - }, - { - $set: { - discount_rule: { type: "fixed", value: 10, allocation: "total" }, - }, - }, - { runValidators: true } - ) }) - it("throws if metadata update is attempted", async () => { + it("throws on invalid discount id", async () => { try { - await discountService.update(IdMap.getId("total10"), { - metadata: { test: "test" }, - }) + await discountService.retrieve(IdMap.getId("invalid")) } catch (error) { - expect(error.message).toEqual( - "Use setMetadata to update discount metadata" + expect(error.message).toBe( + `Discount with ${IdMap.getId("invalid")} was not found` ) } }) }) - describe("addValidVariant", () => { + describe("retrieveByCode", () => { + const discountRepository = MockRepository({ + findOne: query => { + if (query.where.code === "10%OFF") { + return Promise.resolve({ id: IdMap.getId("total10"), code: "10%OFF" }) + } + if (query.where.code === "DYNAMIC") { + return Promise.resolve({ id: IdMap.getId("total10"), code: "10%OFF" }) + } + return Promise.resolve(undefined) + }, + }) + const discountService = new DiscountService({ - discountModel: DiscountModelMock, - productVariantService: ProductVariantServiceMock, + manager: MockManager, + discountRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls model layer updateOne", async () => { - await discountService.addValidVariant( + it("successfully finds discount by code", async () => { + await discountService.retrieveByCode("10%OFF") + expect(discountRepository.findOne).toHaveBeenCalledTimes(1) + expect(discountRepository.findOne).toHaveBeenCalledWith({ + where: { + code: "10%OFF", + is_dynamic: false, + }, + relations: [], + }) + }) + }) + + describe("update", () => { + const discountRepository = MockRepository({ + findOne: () => + Promise.resolve({ id: IdMap.getId("total10"), code: "10%OFF" }), + }) + + const discountRuleRepository = MockRepository({}) + + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("france"), + } + }, + } + + const discountService = new DiscountService({ + manager: MockManager, + discountRepository, + discountRuleRepository, + regionService, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("successfully updates discount", async () => { + await discountService.update(IdMap.getId("total10"), { + code: "test", + regions: [IdMap.getId("france")], + }) + expect(discountRepository.save).toHaveBeenCalledTimes(1) + expect(discountRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("total10"), + code: "test", + regions: [{ id: IdMap.getId("france") }], + }) + }) + + it("successfully updates discount rule", async () => { + await discountService.update(IdMap.getId("total10"), { + rule: { type: "fixed", value: 10, allocation: "total" }, + }) + expect(discountRepository.save).toHaveBeenCalledTimes(1) + expect(discountRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("total10"), + code: "10%OFF", + rule: { type: "fixed", value: 10, allocation: "total" }, + }) + }) + + it("successfully updates metadata", async () => { + await discountService.update(IdMap.getId("total10"), { + metadata: { testKey: "testValue" }, + }) + expect(discountRepository.save).toHaveBeenCalledTimes(1) + expect(discountRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("total10"), + code: "10%OFF", + metadata: { testKey: "testValue" }, + }) + }) + }) + + describe("addValidProduct", () => { + const discountRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("total10"), + rule: { + id: IdMap.getId("test-rule"), + valid_for: [{ id: IdMap.getId("test-product") }], + }, + }), + }) + + const discountRuleRepository = MockRepository({}) + + const productService = { + retrieve: () => { + return { + id: IdMap.getId("test-product-2"), + } + }, + } + + const discountService = new DiscountService({ + manager: MockManager, + discountRepository, + discountRuleRepository, + productService, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("successfully adds a product", async () => { + await discountService.addValidProduct( IdMap.getId("total10"), - IdMap.getId("testVariant") + IdMap.getId("test-product-2") ) - expect(DiscountModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("total10"), - }, - { - $push: { discount_rule: { valid_for: IdMap.getId("testVariant") } }, - }, - { runValidators: true } + expect(discountRuleRepository.save).toHaveBeenCalledTimes(1) + expect(discountRuleRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-rule"), + valid_for: [ + { id: IdMap.getId("test-product") }, + { id: IdMap.getId("test-product-2") }, + ], + }) + }) + + it("successfully resolves if product already exists", async () => { + await discountService.addValidProduct( + IdMap.getId("total10"), + IdMap.getId("test-product") ) + + expect(discountRuleRepository.save).toHaveBeenCalledTimes(0) }) }) describe("removeValidVariant", () => { + const discountRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("total10"), + rule: { + id: IdMap.getId("test-rule"), + valid_for: [{ id: IdMap.getId("test-product") }], + }, + }), + }) + + const discountRuleRepository = MockRepository({}) + + const productService = { + retrieve: () => { + return { + id: IdMap.getId("test-product"), + } + }, + } + const discountService = new DiscountService({ - discountModel: DiscountModelMock, - productVariantService: ProductVariantServiceMock, + manager: MockManager, + discountRepository, + discountRuleRepository, + productService, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls model layer updateOne", async () => { - await discountService.removeValidVariant( + it("successfully removes a product", async () => { + await discountService.removeValidProduct( IdMap.getId("total10"), - IdMap.getId("testVariant") + IdMap.getId("test-product") ) - expect(DiscountModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("total10"), - }, - { - $pull: { discount_rule: { valid_for: IdMap.getId("testVariant") } }, - }, - { runValidators: true } + expect(discountRuleRepository.save).toHaveBeenCalledTimes(1) + expect(discountRuleRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-rule"), + valid_for: [], + }) + }) + + it("successfully resolve if product does not exist", async () => { + await discountService.removeValidProduct( + IdMap.getId("total10"), + IdMap.getId("test-product-2") ) + + expect(discountRuleRepository.save).toHaveBeenCalledTimes(0) }) }) describe("addRegion", () => { + const discountRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("total10"), + regions: [{ id: IdMap.getId("test-region") }], + }), + }) + + const discountRuleRepository = MockRepository({}) + + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("test-region-2"), + } + }, + } + const discountService = new DiscountService({ - discountModel: DiscountModelMock, - regionService: RegionServiceMock, + manager: MockManager, + discountRepository, + discountRuleRepository, + regionService, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls model layer updateOne", async () => { + it("successfully adds a region", async () => { await discountService.addRegion( IdMap.getId("total10"), - IdMap.getId("testRegion") + IdMap.getId("test-region-2") ) - expect(DiscountModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("total10"), - }, - { - $push: { regions: IdMap.getId("testRegion") }, - }, - { runValidators: true } + expect(discountRepository.save).toHaveBeenCalledTimes(1) + expect(discountRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("total10"), + regions: [ + { id: IdMap.getId("test-region") }, + { id: IdMap.getId("test-region-2") }, + ], + }) + }) + + it("successfully resolves if region already exists", async () => { + await discountService.addRegion( + IdMap.getId("total10"), + IdMap.getId("test-region") ) + + expect(discountRepository.save).toHaveBeenCalledTimes(0) + }) + }) + + describe("createDynamicDiscount", () => { + const discountRepository = MockRepository({ + create: d => d, + findOne: () => + Promise.resolve({ + id: "parent", + is_dynamic: true, + rule_id: "parent_rule", + }), + }) + + const discountRuleRepository = MockRepository({}) + + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("test-region"), + } + }, + } + + const discountService = new DiscountService({ + manager: MockManager, + discountRepository, + discountRuleRepository, + regionService, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("successfully removes a region", async () => { + await discountService.createDynamicCode("former", { + code: "hi", + }) + + expect(discountRepository.save).toHaveBeenCalledTimes(1) + expect(discountRepository.save).toHaveBeenCalledWith({ + is_dynamic: true, + is_disabled: false, + rule_id: "parent_rule", + parent_discount_id: "parent", + code: "HI", + }) }) }) describe("removeRegion", () => { + const discountRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("total10"), + regions: [{ id: IdMap.getId("test-region") }], + }), + }) + + const discountRuleRepository = MockRepository({}) + + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("test-region"), + } + }, + } + const discountService = new DiscountService({ - discountModel: DiscountModelMock, - regionService: RegionServiceMock, + manager: MockManager, + discountRepository, + discountRuleRepository, + regionService, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls model layer updateOne", async () => { + it("successfully removes a region", async () => { await discountService.removeRegion( IdMap.getId("total10"), - IdMap.getId("testRegion") + IdMap.getId("test-region") ) - expect(DiscountModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("total10"), - }, - { - $pull: { regions: IdMap.getId("testRegion") }, - }, - { runValidators: true } - ) - }) - }) - - describe("generateGiftCard", () => { - const discountService = new DiscountService({ - discountModel: DiscountModelMock, - regionService: RegionServiceMock, - eventBusService: EventBusServiceMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("calls model layer create", async () => { - await discountService.generateGiftCard(100, IdMap.getId("testRegion")) - - expect(DiscountModelMock.create).toHaveBeenCalledTimes(1) - expect(DiscountModelMock.create).toHaveBeenCalledWith({ - code: expect.stringMatching(/(([A-Z0-9]){4}(-?)){4}/), - is_giftcard: true, - original_amount: 100, - discount_rule: { - type: "fixed", - allocation: "total", - value: 100, - }, - regions: [IdMap.getId("testRegion")], + expect(discountRepository.save).toHaveBeenCalledTimes(1) + expect(discountRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("total10"), + regions: [], }) }) + + it("successfully resolve if region does not exist", async () => { + await discountService.removeRegion( + IdMap.getId("total10"), + IdMap.getId("test-region-2") + ) + + expect(discountRepository.save).toHaveBeenCalledTimes(0) + }) }) }) diff --git a/packages/medusa/src/services/__tests__/document.js b/packages/medusa/src/services/__tests__/document.js deleted file mode 100644 index 94cbe9ad53..0000000000 --- a/packages/medusa/src/services/__tests__/document.js +++ /dev/null @@ -1,24 +0,0 @@ -import DocumentService from "../document" -import { DocumentModelMock } from "../../models/__mocks__/document" -import { IdMap } from "medusa-test-utils" - -describe("DocumentService", () => { - describe("retrieve", () => { - const documentService = new DocumentService({ - documentModel: DocumentModelMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("retrieves a document", async () => { - await documentService.retrieve(IdMap.getId("doc")) - - expect(DocumentModelMock.findOne).toHaveBeenCalledTimes(1) - expect(DocumentModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("doc"), - }) - }) - }) -}) diff --git a/packages/medusa/src/services/__tests__/event-bus.js b/packages/medusa/src/services/__tests__/event-bus.js index defc5b51b5..792d93c43a 100644 --- a/packages/medusa/src/services/__tests__/event-bus.js +++ b/packages/medusa/src/services/__tests__/event-bus.js @@ -1,4 +1,5 @@ import Bull from "bull" +import { MockRepository, MockManager } from "medusa-test-utils" import EventBusService from "../event-bus" import config from "../../config" @@ -16,9 +17,22 @@ const loggerMock = { describe("EventBusService", () => { describe("constructor", () => { + let eventBus beforeAll(() => { jest.resetAllMocks() - const eventBus = new EventBusService({ logger: loggerMock }) + const stagedJobRepository = MockRepository({ + find: () => Promise.resolve([]), + }) + + eventBus = new EventBusService({ + manager: MockManager, + stagedJobRepository, + logger: loggerMock, + }) + }) + + afterAll(async () => { + await await eventBus.stopEnqueuer() }) it("creates bull queue", () => { @@ -34,9 +48,20 @@ describe("EventBusService", () => { describe("successfully adds subscriber", () => { beforeAll(() => { jest.resetAllMocks() - eventBus = new EventBusService({ logger: loggerMock }) + const stagedJobRepository = MockRepository({ + find: () => Promise.resolve([]), + }) + + eventBus = new EventBusService({ + manager: MockManager, + stagedJobRepository, + logger: loggerMock, + }) eventBus.subscribe("eventName", () => "test") }) + afterAll(async () => { + await eventBus.stopEnqueuer() + }) it("added the subscriber to the queue", () => { expect(eventBus.observers_["eventName"].length).toEqual(1) @@ -44,9 +69,21 @@ describe("EventBusService", () => { }) describe("fails when adding non-function subscriber", () => { + let eventBus beforeAll(() => { jest.resetAllMocks() - eventBus = new EventBusService({ logger: loggerMock }) + const stagedJobRepository = MockRepository({ + find: () => Promise.resolve([]), + }) + + eventBus = new EventBusService({ + manager: MockManager, + stagedJobRepository, + logger: loggerMock, + }) + }) + afterAll(async () => { + await eventBus.stopEnqueuer() }) it("rejects subscriber with error", () => { @@ -64,20 +101,27 @@ describe("EventBusService", () => { describe("successfully adds job to queue", () => { beforeAll(() => { jest.resetAllMocks() - eventBus = new EventBusService({ logger: loggerMock }) + const stagedJobRepository = MockRepository({ + find: () => Promise.resolve([]), + }) + + eventBus = new EventBusService({ + logger: loggerMock, + manager: MockManager, + stagedJobRepository, + }) eventBus.queue_.add.mockImplementationOnce(() => "hi") job = eventBus.emit("eventName", { hi: "1234" }) }) + afterAll(async () => { + await eventBus.stopEnqueuer() + }) it("calls queue.add", () => { expect(eventBus.queue_.add).toHaveBeenCalled() }) - - it("returns the job", () => { - expect(job).toEqual("hi") - }) }) }) @@ -86,13 +130,24 @@ describe("EventBusService", () => { describe("successfully runs the worker", () => { beforeAll(async () => { jest.resetAllMocks() - eventBus = new EventBusService({ logger: loggerMock }) + const stagedJobRepository = MockRepository({ + find: () => Promise.resolve([]), + }) + + eventBus = new EventBusService({ + manager: MockManager, + stagedJobRepository, + logger: loggerMock, + }) eventBus.subscribe("eventName", () => Promise.resolve("hi")) result = await eventBus.worker_({ data: { eventName: "eventName", data: {} }, }) }) + afterAll(async () => { + await eventBus.stopEnqueuer() + }) it("calls logger", () => { expect(loggerMock.info).toHaveBeenCalled() expect(loggerMock.info).toHaveBeenCalledWith( @@ -106,9 +161,18 @@ describe("EventBusService", () => { }) describe("continue if errors occur", () => { + let eventBus beforeAll(async () => { jest.resetAllMocks() - eventBus = new EventBusService({ logger: loggerMock }) + const stagedJobRepository = MockRepository({ + find: () => Promise.resolve([]), + }) + + eventBus = new EventBusService({ + manager: MockManager, + stagedJobRepository, + logger: loggerMock, + }) eventBus.subscribe("eventName", () => Promise.resolve("hi")) eventBus.subscribe("eventName", () => Promise.resolve("hi2")) eventBus.subscribe("eventName", () => Promise.resolve("hi3")) @@ -121,6 +185,9 @@ describe("EventBusService", () => { }) }) + afterAll(async () => { + await eventBus.stopEnqueuer() + }) it("calls logger warn on rejections", () => { expect(loggerMock.warn).toHaveBeenCalledTimes(3) expect(loggerMock.warn).toHaveBeenCalledWith( diff --git a/packages/medusa/src/services/__tests__/fulfillment.js b/packages/medusa/src/services/__tests__/fulfillment.js new file mode 100644 index 0000000000..2d20deadf0 --- /dev/null +++ b/packages/medusa/src/services/__tests__/fulfillment.js @@ -0,0 +1,129 @@ +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import FulfillmentService from "../fulfillment" + +describe("FulfillmentService", () => { + describe("createFulfillment", () => { + const fulfillmentRepository = MockRepository({}) + + const fulfillmentProviderService = { + createFulfillment: jest.fn().mockImplementation(data => { + return Promise.resolve(data) + }), + } + + const shippingProfileService = { + retrieve: jest.fn().mockImplementation(data => { + return Promise.resolve({ + id: IdMap.getId("default"), + name: "default_profile", + products: [IdMap.getId("product")], + shipping_options: [], + }) + }), + } + + const fulfillmentService = new FulfillmentService({ + manager: MockManager, + fulfillmentProviderService, + fulfillmentRepository, + shippingProfileService, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully create a fulfillment", async () => { + await fulfillmentService.createFulfillment( + { + shipping_methods: [ + { + shipping_option: { + profile_id: IdMap.getId("default"), + provider_id: "GLS Express", + }, + }, + ], + items: [{ id: IdMap.getId("test-line"), quantity: 10 }], + }, + [ + { + item_id: IdMap.getId("test-line"), + quantity: 10, + }, + ], + { order_id: "test", metadata: {} } + ) + + expect(fulfillmentRepository.create).toHaveBeenCalledTimes(1) + expect(fulfillmentRepository.create).toHaveBeenCalledWith({ + order_id: "test", + provider_id: "GLS Express", + items: [{ item_id: IdMap.getId("test-line"), quantity: 10 }], + data: expect.anything(), + metadata: {}, + }) + }) + + it("throws if too many items are requested fulfilled", async () => { + await expect( + fulfillmentService.createFulfillment( + { + shipping_methods: [ + { + profile_id: IdMap.getId("default"), + provider_id: "GLS Express", + }, + ], + items: [ + { + id: IdMap.getId("test-line"), + quantity: 10, + fulfilled_quantity: 0, + }, + ], + }, + [ + { + item_id: IdMap.getId("test-line"), + quantity: 12, + }, + ] + ) + ).rejects.toThrow("Cannot fulfill more items than have been purchased") + }) + }) + + describe("createShipment", () => { + const fulfillmentRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("fulfillment") }), + }) + + const fulfillmentService = new FulfillmentService({ + manager: MockManager, + fulfillmentRepository, + }) + + const now = new Date() + beforeEach(async () => { + jest.clearAllMocks() + jest.spyOn(global, "Date").mockImplementationOnce(() => now) + }) + + it("calls order model functions", async () => { + await fulfillmentService.createShipment( + IdMap.getId("fulfillment"), + ["1234", "2345"], + {} + ) + + expect(fulfillmentRepository.save).toHaveBeenCalledTimes(1) + expect(fulfillmentRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("fulfillment"), + tracking_numbers: ["1234", "2345"], + metadata: {}, + shipped_at: now, + }) + }) + }) +}) diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index 7904e28ea8..1a9e078981 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -1,96 +1,254 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import LineItemService from "../line-item" -import { ProductVariantServiceMock } from "../__mocks__/product-variant" -import { ProductServiceMock } from "../__mocks__/product" -import { RegionServiceMock } from "../__mocks__/region" describe("LineItemService", () => { - describe("generate", () => { - let result - beforeAll(async () => { - jest.clearAllMocks() - const lineItemService = new LineItemService({ - productVariantService: ProductVariantServiceMock, - productService: ProductServiceMock, - regionService: RegionServiceMock, - }) - result = await lineItemService.generate( - IdMap.getId("eur-10-us-12"), - IdMap.getId("region-france"), - 2 - ) + describe("create", () => { + const lineItemRepository = MockRepository({}) + + const cartRepository = MockRepository({ + findOne: () => + Promise.resolve({ + region_id: IdMap.getId("test-region"), + }), }) - it("generates line item and successfully defaults quantity of content to 1", () => { - expect(result).toEqual({ - title: "test", - description: "EUR10US-12", - thumbnail: "test.1234", - should_merge: true, - content: { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - title: "EUR10US-12", - }, + const regionService = { + retrieve: () => { + return { + id: IdMap.getId("test-region"), + } + }, + } + + const productVariantService = { + retrieve: query => { + if (query === IdMap.getId("test-giftcard")) { + return { + id: IdMap.getId("test-giftcard"), + title: "Test variant", + product: { + title: "Test product", + thumbnail: "", + is_giftcard: true, + }, + } + } + return { + id: IdMap.getId("test-variant"), + title: "Test variant", product: { - _id: "1234", - title: "test", - thumbnail: "test.1234", + title: "Test product", + thumbnail: "", }, - quantity: 1, - }, + } + }, + getRegionPrice: () => 100, + } + + const lineItemService = new LineItemService({ + manager: MockManager, + lineItemRepository, + productVariantService, + regionService, + cartRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully create a line item", async () => { + await lineItemService.create({ + variant_id: IdMap.getId("test-variant"), + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 100, + quantity: 1, + }) + + expect(lineItemRepository.create).toHaveBeenCalledTimes(1) + expect(lineItemRepository.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("test-variant"), + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 100, + quantity: 1, + }) + }) + + it("successfully create a line item with price and quantity", async () => { + await lineItemService.create({ + variant_id: IdMap.getId("test-variant"), + cart_id: IdMap.getId("test-cart"), + unit_price: 50, quantity: 2, + }) + + expect(lineItemRepository.create).toHaveBeenCalledTimes(1) + expect(lineItemRepository.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("test-variant"), + cart_id: IdMap.getId("test-cart"), + unit_price: 50, + quantity: 2, + }) + }) + + it("successfully create a line item giftcard", async () => { + const line = await await lineItemService.generate( + IdMap.getId("test-giftcard"), + IdMap.getId("test-region") + ) + + await lineItemService.create({ + ...line, + cart_id: IdMap.getId("test-cart"), + }) + + expect(lineItemRepository.create).toHaveBeenCalledTimes(1) + expect(lineItemRepository.create).toHaveBeenCalledWith({ + allow_discounts: false, + variant_id: IdMap.getId("test-giftcard"), + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 100, + quantity: 1, + is_giftcard: true, + should_merge: true, metadata: {}, }) }) }) - describe("generate with giftcard", () => { - let result - beforeAll(async () => { - jest.clearAllMocks() - const lineItemService = new LineItemService({ - productVariantService: ProductVariantServiceMock, - productService: ProductServiceMock, - regionService: RegionServiceMock, - }) - result = await lineItemService.generate( - IdMap.getId("giftCardVar"), - IdMap.getId("region-france"), - 1, - { - name: "Test Name", - } - ) + describe("update", () => { + const lineItemRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", + }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, + quantity: 1, + }), }) - it("results correctly", () => { - expect(result).toEqual({ - title: "Gift Card", - description: "100 USD", - thumbnail: "1234", - is_giftcard: true, - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("giftCardVar"), - title: "100 USD", - }, - product: { - _id: IdMap.getId("giftCardProd"), - title: "Gift Card", - thumbnail: "1234", - is_giftcard: true, - }, - quantity: 1, + const lineItemService = new LineItemService({ + manager: MockManager, + lineItemRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully updates a line item with quantity", async () => { + await lineItemService.update(IdMap.getId("test-line-item"), { + quantity: 2, + has_shipping: true, + }) + + expect(lineItemRepository.save).toHaveBeenCalledTimes(1) + expect(lineItemRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, + quantity: 2, + has_shipping: true, + }) + }) + + it("successfully updates a line item with metadata", async () => { + await lineItemService.update(IdMap.getId("test-line-item"), { metadata: { - name: "Test Name", + testKey: "testValue", }, + }) + + expect(lineItemRepository.save).toHaveBeenCalledTimes(1) + expect(lineItemRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", + }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, + quantity: 1, + metadata: { + testKey: "testValue", + }, + }) + }) + }) + describe("delete", () => { + const lineItemRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", + }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, + quantity: 1, + }), + }) + + const lineItemService = new LineItemService({ + manager: MockManager, + lineItemRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully deletes", async () => { + await lineItemService.delete(IdMap.getId("test-line-item")) + + expect(lineItemRepository.remove).toHaveBeenCalledTimes(1) + expect(lineItemRepository.remove).toHaveBeenCalledWith({ + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", + }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, quantity: 1, - should_merge: true, }) }) }) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index d041a645f7..3ee3037e54 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1,30 +1,30 @@ -import { IdMap } from "medusa-test-utils" -import { OrderModelMock, orders } from "../../models/__mocks__/order" -import { carts } from "../../models/__mocks__/cart" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import OrderService from "../order" -import ReturnService from "../return" -import FulfillmentService from "../fulfillment" -import { - PaymentProviderServiceMock, - DefaultProviderMock, -} from "../__mocks__/payment-provider" -import { DiscountServiceMock } from "../__mocks__/discount" -import { - FulfillmentProviderServiceMock, - DefaultProviderMock as FulfillmentProviderMock, -} from "../__mocks__/fulfillment-provider" -import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" -import { ShippingOptionServiceMock } from "../__mocks__/shipping-option" -import { TotalsServiceMock } from "../__mocks__/totals" -import { RegionServiceMock } from "../__mocks__/region" -import { CounterServiceMock } from "../__mocks__/counter" -import { EventBusServiceMock } from "../__mocks__/event-bus" describe("OrderService", () => { + const totalsService = { + getLineItemRefund: () => {}, + getTotal: o => { + return o.total || 0 + }, + getRefundedTotal: o => { + return o.refunded_total || 0 + }, + } + const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, + } + describe("create", () => { + const orderRepo = MockRepository({ create: f => f }) const orderService = new OrderService({ - orderModel: OrderModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + orderRepository: orderRepo, + totalsService, + eventBusService, }) beforeEach(async () => { @@ -36,22 +36,104 @@ describe("OrderService", () => { email: "oliver@test.dk", }) - expect(OrderModelMock.create).toHaveBeenCalledTimes(1) - expect(OrderModelMock.create).toHaveBeenCalledWith({ + expect(orderRepo.create).toHaveBeenCalledTimes(1) + expect(orderRepo.create).toHaveBeenCalledWith({ + email: "oliver@test.dk", + }) + + expect(orderRepo.save).toHaveBeenCalledWith({ email: "oliver@test.dk", }) }) }) describe("createFromCart", () => { + const orderRepo = MockRepository({ + create: p => p, + save: p => ({ ...p, id: "id" }), + }) + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const shippingOptionService = { + updateShippingMethod: jest.fn(), + withTransaction: function() { + return this + }, + } + const giftCardService = { + update: jest.fn(), + createTransaction: jest.fn(), + withTransaction: function() { + return this + }, + } + const paymentProviderService = { + getStatus: payment => { + return Promise.resolve(payment.status || "authorized") + }, + updatePayment: jest.fn(), + withTransaction: function() { + return this + }, + } + const emptyCart = { + region: {}, + items: [], + total: 0, + } + const cartService = { + retrieve: jest.fn().mockImplementation(query => { + if (query === "empty") { + return Promise.resolve(emptyCart) + } + return Promise.resolve({ + id: "cart_id", + email: "test@test.com", + customer_id: "cus_1234", + payment: { + id: "testpayment", + amount: 100, + status: "authorized", + }, + region_id: "test", + region: { + id: "test", + currency_code: "eur", + name: "test", + tax_rate: 25, + }, + shipping_address_id: "1234", + billing_address_id: "1234", + discounts: [], + gift_cards: [], + shipping_methods: [{ id: "method_1" }], + items: [{ id: "item_1" }, { id: "item_2" }], + total: 100, + }) + }), + withTransaction: function() { + return this + }, + } + + const discountService = {} + const regionService = {} const orderService = new OrderService({ - orderModel: OrderModelMock, - paymentProviderService: PaymentProviderServiceMock, - totalsService: TotalsServiceMock, - discountService: DiscountServiceMock, - regionService: RegionServiceMock, - eventBusService: EventBusServiceMock, - counterService: CounterServiceMock, + manager: MockManager, + orderRepository: orderRepo, + lineItemService, + giftCardService, + paymentProviderService, + shippingOptionService, + totalsService, + discountService, + regionService, + eventBusService, + cartService, }) beforeEach(async () => { @@ -59,170 +141,317 @@ describe("OrderService", () => { }) it("fails when no items", async () => { - const res = orderService.createFromCart(carts.emptyCart) - expect(res).rejects.toThrow("Cannot create order from empty cart") + const res = orderService.createFromCart("empty") + await expect(res).rejects.toThrow("Cannot create order from empty cart") }) it("calls order model functions", async () => { - await orderService.createFromCart({ - ...carts.completeCart, + const cart = { + id: "cart_id", + email: "test@test.com", + customer_id: "cus_1234", + payment: { + id: "testpayment", + amount: 100, + status: "authorized", + }, + region_id: "test", + region: { + id: "test", + currency_code: "eur", + name: "test", + tax_rate: 25, + }, + shipping_address_id: "1234", + billing_address_id: "1234", + gift_cards: [], + discounts: [], + shipping_methods: [{ id: "method_1" }], + items: [{ id: "item_1" }, { id: "item_2" }], total: 100, - }) + } + orderService.cartService_.retrieve = jest.fn(() => Promise.resolve(cart)) + + await orderService.createFromCart("cart_id") const order = { - ...carts.completeCart, + payment_status: "awaiting", + email: cart.email, + customer_id: cart.customer_id, + shipping_methods: cart.shipping_methods, + customer_id: "cus_1234", + discounts: cart.discounts, + billing_address_id: cart.billing_address_id, + shipping_address_id: cart.shipping_address_id, + region_id: cart.region_id, currency_code: "eur", - cart_id: carts.completeCart._id, - tax_rate: 0.25, + cart_id: "cart_id", + tax_rate: 25, + gift_cards: [], metadata: {}, } - delete order._id - delete order.payment_sessions - expect(OrderModelMock.create).toHaveBeenCalledTimes(1) - expect(OrderModelMock.create).toHaveBeenCalledWith([order], { - session: expect.anything(), + expect(cartService.retrieve).toHaveBeenCalledTimes(1) + expect(cartService.retrieve).toHaveBeenCalledWith("cart_id", { + select: ["subtotal", "total"], + relations: [ + "region", + "payment", + "items", + "discounts", + "gift_cards", + "shipping_methods", + ], }) + + expect(paymentProviderService.updatePayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.updatePayment).toHaveBeenCalledWith( + "testpayment", + { + order_id: "id", + } + ) + + expect(lineItemService.update).toHaveBeenCalledTimes(2) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + order_id: "id", + }) + expect(lineItemService.update).toHaveBeenCalledWith("item_2", { + order_id: "id", + }) + + expect(orderRepo.create).toHaveBeenCalledTimes(1) + expect(orderRepo.create).toHaveBeenCalledWith(order) + expect(orderRepo.save).toHaveBeenCalledWith(order) + }) + + it("creates gift card transactions", async () => { + const cart = { + id: "cart_id", + email: "test@test.com", + customer_id: "cus_1234", + payment: { + id: "testpayment", + amount: 100, + status: "authorized", + }, + region_id: "test", + region: { + id: "test", + currency_code: "eur", + name: "test", + tax_rate: 25, + }, + shipping_address_id: "1234", + billing_address_id: "1234", + gift_cards: [ + { + id: "gid", + code: "GC", + balance: 80, + }, + ], + discounts: [], + shipping_methods: [{ id: "method_1" }], + items: [{ id: "item_1" }, { id: "item_2" }], + subtotal: 100, + total: 100, + } + + orderService.cartService_.retrieve = () => { + return Promise.resolve(cart) + } + + await orderService.createFromCart("cart_id") + const order = { + payment_status: "awaiting", + email: cart.email, + customer_id: cart.customer_id, + shipping_methods: cart.shipping_methods, + customer_id: "cus_1234", + discounts: cart.discounts, + billing_address_id: cart.billing_address_id, + shipping_address_id: cart.shipping_address_id, + region_id: cart.region_id, + currency_code: "eur", + cart_id: "cart_id", + tax_rate: 25, + gift_cards: [ + { + id: "gid", + code: "GC", + balance: 80, + }, + ], + metadata: {}, + } + + expect(giftCardService.update).toHaveBeenCalledTimes(1) + expect(giftCardService.update).toHaveBeenCalledWith("gid", { + balance: 0, + disabled: true, + }) + + expect(giftCardService.createTransaction).toHaveBeenCalledTimes(1) + expect(giftCardService.createTransaction).toHaveBeenCalledWith({ + gift_card_id: "gid", + order_id: "id", + amount: 80, + }) + + expect(paymentProviderService.updatePayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.updatePayment).toHaveBeenCalledWith( + "testpayment", + { + order_id: "id", + } + ) + + expect(lineItemService.update).toHaveBeenCalledTimes(2) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + order_id: "id", + }) + expect(lineItemService.update).toHaveBeenCalledWith("item_2", { + order_id: "id", + }) + + expect(orderRepo.create).toHaveBeenCalledTimes(1) + expect(orderRepo.create).toHaveBeenCalledWith(order) + expect(orderRepo.save).toHaveBeenCalledWith(order) }) it("creates cart with 0 total", async () => { - await orderService.createFromCart({ - ...carts.completeCart, + const cart = { + id: "cart_id", + email: "test@test.com", + customer_id: "cus_1234", + payment: { + id: "testpayment", + amount: 100, + status: "authorized", + }, + region_id: "test", + region: { + id: "test", + currency_code: "eur", + name: "test", + tax_rate: 25, + }, + gift_cards: [], + shipping_address_id: "1234", + billing_address_id: "1234", + discounts: [], + shipping_methods: [{ id: "method_1" }], + items: [{ id: "item_1" }, { id: "item_2" }], total: 0, - }) - + } + orderService.cartService_.retrieve = () => Promise.resolve(cart) + await orderService.createFromCart(cart) const order = { - ...carts.completeCart, - payment_method: {}, + payment_status: "awaiting", + email: cart.email, + customer_id: cart.customer_id, + shipping_methods: cart.shipping_methods, + customer_id: "cus_1234", + discounts: cart.discounts, + billing_address_id: cart.billing_address_id, + shipping_address_id: cart.shipping_address_id, + gift_cards: [], + region_id: cart.region_id, currency_code: "eur", - cart_id: carts.completeCart._id, - tax_rate: 0.25, + cart_id: "cart_id", + tax_rate: 25, metadata: {}, } - delete order._id - delete order.payment_sessions + expect(orderRepo.create).toHaveBeenCalledTimes(1) + expect(orderRepo.create).toHaveBeenCalledWith(order) - expect(OrderModelMock.create).toHaveBeenCalledTimes(1) - expect(OrderModelMock.create).toHaveBeenCalledWith([order], { - session: expect.anything(), + expect(lineItemService.update).toHaveBeenCalledTimes(2) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + order_id: "id", }) - }) - - it("creates cart with gift card", async () => { - await orderService.createFromCart({ - ...carts.withGiftCard, - total: 100, + expect(lineItemService.update).toHaveBeenCalledWith("item_2", { + order_id: "id", }) - const order = { - ...carts.withGiftCard, - metadata: {}, - items: [ - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - is_giftcard: false, - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - quantity: 10, - }, - { - _id: IdMap.getId("giftline"), - title: "GiftCard", - description: "Gift card line", - thumbnail: "test-img-yeah.com/thumb", - metadata: { - name: "Test Name", - }, - is_giftcard: true, - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("giftCardVar"), - }, - product: { - _id: IdMap.getId("giftCardProd"), - }, - quantity: 1, - }, - quantity: 1, - }, - ], - currency_code: "eur", - cart_id: carts.withGiftCard._id, - tax_rate: 0.25, - } - - delete order._id - delete order.payment_sessions - - expect(OrderModelMock.create).toHaveBeenCalledTimes(1) - expect(OrderModelMock.create).toHaveBeenCalledWith([order], { - session: expect.anything(), - }) + expect(orderRepo.save).toHaveBeenCalledWith(order) }) }) describe("retrieve", () => { - let result + const orderRepo = MockRepository({ + findOne: q => { + return Promise.resolve({}) + }, + }) const orderService = new OrderService({ - orderModel: OrderModelMock, + manager: MockManager, + orderRepository: orderRepo, + totalsService, }) beforeAll(async () => { jest.clearAllMocks() - result = await orderService.retrieve(IdMap.getId("test-order")) }) it("calls order model functions", async () => { - expect(OrderModelMock.findOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("test-order"), + await orderService.retrieve(IdMap.getId("test-order")) + expect(orderRepo.findOne).toHaveBeenCalledTimes(1) + expect(orderRepo.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("test-order") }, }) }) - - it("returns correct order", async () => { - expect(result._id).toEqual(IdMap.getId("test-order")) - }) }) describe("retrieveByCartId", () => { - let result + const orderRepo = MockRepository({ + findOne: q => { + return Promise.resolve({}) + }, + }) const orderService = new OrderService({ - orderModel: OrderModelMock, + totalsService, + manager: MockManager, + orderRepository: orderRepo, }) beforeAll(async () => { jest.clearAllMocks() - result = await orderService.retrieveByCartId(IdMap.getId("test-cart")) }) it("calls order model functions", async () => { - expect(OrderModelMock.findOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.findOne).toHaveBeenCalledWith({ - cart_id: IdMap.getId("test-cart"), + await orderService.retrieveByCartId(IdMap.getId("test-cart")) + expect(orderRepo.findOne).toHaveBeenCalledTimes(1) + expect(orderRepo.findOne).toHaveBeenCalledWith({ + where: { cart_id: IdMap.getId("test-cart") }, }) }) - - it("returns correct order", async () => { - expect(result._id).toEqual(IdMap.getId("test-order")) - }) }) describe("update", () => { + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("fulfilled-order"): + return Promise.resolve({ + fulfillment_status: "fulfilled", + payment_status: "awaiting", + status: "pending", + }) + default: + return Promise.resolve({ + fulfillment_status: "not_fulfilled", + payment_status: "awaiting", + status: "pending", + }) + } + }, + }) const orderService = new OrderService({ - orderModel: OrderModelMock, - eventBusService: EventBusServiceMock, + totalsService, + manager: MockManager, + orderRepository: orderRepo, + eventBusService, }) beforeEach(async () => { @@ -234,126 +463,95 @@ describe("OrderService", () => { email: "oliver@test.dk", }) - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("test-order") }, - { - $set: { - email: "oliver@test.dk", - }, - }, - { runValidators: true } - ) - }) - - it("throws on invalid billing address", async () => { - const address = { - last_name: "James", - address_1: "24 Dunks Drive", - city: "Los Angeles", - country_code: "US", - province: "CA", - postal_code: "93011", - } - - try { - await orderService.update(IdMap.getId("test-order"), { - billing_address: address, - }) - } catch (err) { - expect(err.message).toEqual("The address is not valid") - } - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(0) - }) - - it("throws on invalid shipping address", async () => { - const address = { - last_name: "James", - address_1: "24 Dunks Drive", - city: "Los Angeles", - country_code: "US", - province: "CA", - postal_code: "93011", - } - - try { - await orderService.update(IdMap.getId("test-order"), { - shipping_address: address, - }) - } catch (err) { - expect(err.message).toEqual("The address is not valid") - } - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(0) + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + email: "oliver@test.dk", + fulfillment_status: "not_fulfilled", + payment_status: "awaiting", + status: "pending", + }) }) it("throws if metadata update are attempted", async () => { - try { - await orderService.update(IdMap.getId("test-order"), { - metadata: { test: "foo" }, - }) - } catch (error) { - expect(error.message).toEqual( - "Use setMetadata to update metadata fields" - ) - } - }) - - it("throws if address updates are attempted after fulfillment", async () => { - try { - await orderService.update(IdMap.getId("fulfilled-order"), { - billing_address: { - first_name: "Lebron", - last_name: "James", - address_1: "24 Dunks Drive", - city: "Los Angeles", - country_code: "US", - province: "CA", - postal_code: "93011", - }, - }) - } catch (error) { - expect(error.message).toEqual( - "Can't update shipping, billing, items and payment method when order is processed" - ) - } + await orderService.update(IdMap.getId("test-order"), { + metadata: { test: "foo" }, + }) + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + metadata: { test: "foo" }, + fulfillment_status: "not_fulfilled", + payment_status: "awaiting", + status: "pending", + }) }) it("throws if payment method update is attempted after fulfillment", async () => { - try { - await orderService.update(IdMap.getId("fulfilled-order"), { - payment_method: { + await expect( + orderService.update(IdMap.getId("fulfilled-order"), { + payment: { provider_id: "test", profile_id: "test", }, }) - } catch (error) { - expect(error.message).toEqual( - "Can't update shipping, billing, items and payment method when order is processed" - ) - } + ).rejects.toThrow( + "Can't update shipping, billing, items and payment method when order is processed" + ) }) it("throws if items update is attempted after fulfillment", async () => { - try { - await orderService.update(IdMap.getId("fulfilled-order"), { + await expect( + orderService.update(IdMap.getId("fulfilled-order"), { items: [], }) - } catch (error) { - expect(error.message).toEqual( - "Can't update shipping, billing, items and payment method when order is processed" - ) - } + ).rejects.toThrow( + "Can't update shipping, billing, items and payment method when order is processed" + ) }) }) describe("cancel", () => { + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("paid-order"): + return Promise.resolve({ + fulfillment_status: "fulfilled", + payment_status: "paid", + status: "pending", + }) + default: + return Promise.resolve({ + fulfillment_status: "not_fulfilled", + payment_status: "awaiting", + status: "pending", + fulfillments: [{ id: "fulfillment_test" }], + payments: [{ id: "payment_test" }], + }) + } + }, + }) + + const fulfillmentService = { + cancelFulfillment: jest.fn(), + withTransaction: function() { + return this + }, + } + + const paymentProviderService = { + cancelPayment: jest.fn(), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ - fulfillmentProviderService: FulfillmentProviderServiceMock, - paymentProviderService: PaymentProviderServiceMock, - orderModel: OrderModelMock, - eventBusService: EventBusServiceMock, + totalsService, + manager: MockManager, + orderRepository: orderRepo, + paymentProviderService, + fulfillmentService, + eventBusService, }) beforeEach(async () => { @@ -363,55 +561,74 @@ describe("OrderService", () => { it("calls order model functions", async () => { await orderService.cancel(IdMap.getId("not-fulfilled-order")) - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("not-fulfilled-order") }, - { - $set: { - status: "canceled", - fulfillment_status: "canceled", - payment_status: "canceled", - fulfillments: [ - { - _id: IdMap.getId("fulfillment"), - data: {}, - is_canceled: true, - provider_id: "default_provider", - }, - ], - payment_method: { - data: {}, - provider_id: "default_provider", - }, - }, - } - ) - }) + expect(paymentProviderService.cancelPayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.cancelPayment).toHaveBeenCalledWith({ + id: "payment_test", + }) - it("throws if order is fulfilled", async () => { - try { - await orderService.cancel(IdMap.getId("fulfilled-order")) - } catch (error) { - expect(error.message).toEqual("Can't cancel a fulfilled order") - } + expect(fulfillmentService.cancelFulfillment).toHaveBeenCalledTimes(1) + expect(fulfillmentService.cancelFulfillment).toHaveBeenCalledWith({ + id: "fulfillment_test", + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + fulfillment_status: "canceled", + payment_status: "canceled", + status: "canceled", + fulfillments: [{ id: "fulfillment_test" }], + payments: [{ id: "payment_test" }], + }) }) it("throws if order payment is captured", async () => { - try { - await orderService.cancel(IdMap.getId("payed-order")) - } catch (error) { - expect(error.message).toEqual( - "Can't cancel an order with a processed payment" - ) - } + await expect( + orderService.cancel(IdMap.getId("paid-order")) + ).rejects.toThrow("Can't cancel an order with a processed payment") }) }) describe("capturePayment", () => { + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("fail"): + return Promise.resolve({ + payment_status: "awaiting", + payments: [{ id: "payment_fail", captured_at: null }], + }) + + default: + return Promise.resolve({ + fulfillment_status: "not_fulfilled", + payment_status: "awaiting", + status: "pending", + fulfillments: [{ id: "fulfillment_test" }], + payments: [{ id: "payment_test", captured_at: null }], + }) + } + }, + }) + + const paymentProviderService = { + capturePayment: jest + .fn() + .mockImplementation(p => + p.id === "payment_fail" + ? Promise.reject() + : Promise.resolve({ ...p, captured_at: "notnull" }) + ), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ - orderModel: OrderModelMock, - paymentProviderService: PaymentProviderServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + orderRepository: orderRepo, + paymentProviderService, + totalsService, + eventBusService, }) beforeEach(async () => { @@ -419,33 +636,109 @@ describe("OrderService", () => { }) it("calls order model functions", async () => { - await orderService.capturePayment(IdMap.getId("test-order")) + await orderService.capturePayment("test-order") - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("test-order") }, - { $set: { payment_status: "captured" } } - ) + expect(paymentProviderService.capturePayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.capturePayment).toHaveBeenCalledWith({ + id: "payment_test", + captured_at: null, + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + fulfillment_status: "not_fulfilled", + payment_status: "captured", + status: "pending", + fulfillments: [{ id: "fulfillment_test" }], + payments: [{ id: "payment_test", captured_at: "notnull" }], + }) }) - it("throws if payment is already processed", async () => { - await expect( - orderService.capturePayment(IdMap.getId("payed-order")) - ).rejects.toThrow("Payment already captured") + it("sets requires action on failure", async () => { + await orderService.capturePayment(IdMap.getId("fail")) + + expect(paymentProviderService.capturePayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.capturePayment).toHaveBeenCalledWith({ + id: "payment_fail", + captured_at: null, + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + payment_status: "requires_action", + payments: [{ id: "payment_fail", captured_at: null }], + }) }) }) describe("createFulfillment", () => { - const fulfillmentService = new FulfillmentService({ - fulfillmentProviderService: FulfillmentProviderServiceMock, - shippingProfileService: ShippingProfileServiceMock, - totalsService: TotalsServiceMock, + const partialOrder = { + fulfillments: [], + shipping_methods: [{ id: "ship" }], + items: [ + { + id: "item_1", + quantity: 2, + fulfilled_quantity: 0, + }, + { + id: "item_2", + quantity: 1, + fulfilled_quantity: 0, + }, + ], + } + + const order = { + fulfillments: [], + shipping_methods: [{ id: "ship" }], + items: [ + { + id: "item_1", + quantity: 2, + fulfilled_quantity: 0, + }, + ], + } + + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case "partial": + return Promise.resolve(partialOrder) + default: + return Promise.resolve(order) + } + }, }) + + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + + const fulfillmentService = { + createFulfillment: jest.fn().mockImplementation((o, i, m) => { + return Promise.resolve([ + { + items: i, + }, + ]) + }), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ - orderModel: OrderModelMock, - paymentProviderService: PaymentProviderServiceMock, + manager: MockManager, + orderRepository: orderRepo, fulfillmentService, - eventBusService: EventBusServiceMock, + lineItemService, + totalsService, + eventBusService, }) beforeEach(async () => { @@ -453,132 +746,166 @@ describe("OrderService", () => { }) it("calls order model functions", async () => { - await orderService.createFulfillment(IdMap.getId("test-order"), [ + await orderService.createFulfillment("test-order", [ { - item_id: IdMap.getId("existingLine"), - quantity: 10, + item_id: "item_1", + quantity: 2, }, ]) - expect(FulfillmentProviderMock.createOrder).toHaveBeenCalledTimes(1) - expect(FulfillmentProviderMock.createOrder).toHaveBeenCalledWith( - { - extra: "hi", - }, + expect(fulfillmentService.createFulfillment).toHaveBeenCalledTimes(1) + expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith( + order, [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - fulfilled_quantity: 0, - quantity: 10, + item_id: "item_1", + quantity: 2, }, ], - orders.testOrder + { metadata: {}, order_id: "test-order" } ) - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("test-order") }, - { - $addToSet: { - fulfillments: { - $each: [ - { - data: { - extra: "hi", - }, - items: [ - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - fulfilled_quantity: 0, - quantity: 10, - }, - ], - metadata: {}, - provider_id: "default_provider", - }, - ], - }, - }, - $set: { - items: [ - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - quantity: 10, - fulfilled_quantity: 10, - fulfilled: true, - }, - ], - fulfillment_status: "fulfilled", - }, - } - ) + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + fulfilled_quantity: 2, + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...order, + fulfillment_status: "fulfilled", + }) }) - it("throws if too many items are requested fulfilled", async () => { - await expect( - orderService.createFulfillment(IdMap.getId("test-order"), [ + it("sets partially fulfilled", async () => { + await orderService.createFulfillment("partial", [ + { + item_id: "item_1", + quantity: 2, + }, + ]) + + expect(fulfillmentService.createFulfillment).toHaveBeenCalledTimes(1) + expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith( + partialOrder, + [ { - item_id: IdMap.getId("existingLine"), - quantity: 11, + item_id: "item_1", + quantity: 2, }, - ]) - ).rejects.toThrow("Cannot fulfill more items than have been purchased") + ], + { metadata: {}, order_id: "partial" } + ) + + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + fulfilled_quantity: 2, + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...partialOrder, + fulfillment_status: "partially_fulfilled", + }) + }) + + it("sets partially fulfilled", async () => { + await orderService.createFulfillment("test", [ + { + item_id: "item_1", + quantity: 1, + }, + ]) + + expect(fulfillmentService.createFulfillment).toHaveBeenCalledTimes(1) + expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith( + order, + [ + { + item_id: "item_1", + quantity: 1, + }, + ], + { metadata: {}, order_id: "test" } + ) + + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + fulfilled_quantity: 1, + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...order, + fulfillment_status: "partially_fulfilled", + }) }) }) describe("receiveReturn", () => { - const returnService = new ReturnService({ - totalsService: TotalsServiceMock, - shippingOptionService: ShippingOptionServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, + const order = { + items: [ + { + id: "item_1", + quantity: 10, + returned_quantity: 0, + }, + ], + payments: [{ id: "payment_test" }], + } + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + default: + return Promise.resolve(order) + } + }, }) + + const returnService = { + retrieve: () => { + return Promise.resolve({ + order_id: IdMap.getId("order"), + }) + }, + receiveReturn: jest + .fn() + .mockImplementation((id, items, amount, mism) => + id === IdMap.getId("good") + ? Promise.resolve({ items, status: "received", refund_amount: 100 }) + : Promise.resolve({ status: "requires_action" }) + ), + withTransaction: function() { + return this + }, + } + + const paymentProviderService = { + refundPayment: jest + .fn() + .mockImplementation(p => + p.id === "payment_fail" ? Promise.reject() : Promise.resolve() + ), + withTransaction: function() { + return this + }, + } + + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ - orderModel: OrderModelMock, + manager: MockManager, + orderRepository: orderRepo, + paymentProviderService, + totalsService, returnService, - shippingOptionService: ShippingOptionServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - paymentProviderService: PaymentProviderServiceMock, - totalsService: TotalsServiceMock, - eventBusService: EventBusServiceMock, + lineItemService, + eventBusService, }) beforeEach(async () => { @@ -586,323 +913,93 @@ describe("OrderService", () => { }) it("calls order model functions", async () => { - await orderService.receiveReturn( - IdMap.getId("returned-order"), - IdMap.getId("return"), - [ - { - item_id: IdMap.getId("existingLine"), - quantity: 10, - }, - ] - ) - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("returned-order") }, + const items = [ { - $push: { - refunds: { - amount: 1228, - }, - }, - $set: { - returns: [ - { - _id: IdMap.getId("return"), - status: "received", - documents: ["doc1234"], - shipping_method: { - _id: IdMap.getId("return-shipping"), - is_return: true, - name: "Return Shipping", - region_id: IdMap.getId("region-france"), - profile_id: IdMap.getId("default-profile"), - data: { - id: "return_shipment", - }, - price: 2, - provider_id: "default_provider", - }, - shipping_data: { - id: "return_shipment", - shipped: true, - }, - items: [ - { - item_id: IdMap.getId("existingLine"), - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - is_requested: true, - is_registered: true, - quantity: 10, - requested_quantity: 10, - }, - ], - refund_amount: 1228, - }, - ], - items: [ - { - _id: IdMap.getId("existingLine"), - content: { - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - }, - description: "This is a new line", - quantity: 10, - returned_quantity: 10, - thumbnail: "test-img-yeah.com/thumb", - title: "merge line", - returned: true, - }, - ], - fulfillment_status: "returned", - }, - } + item_id: "item_1", + quantity: 10, + }, + ] + await orderService.receiveReturn( + IdMap.getId("order"), + IdMap.getId("good"), + items ) - expect(DefaultProviderMock.refundPayment).toHaveBeenCalledTimes(1) - expect(DefaultProviderMock.refundPayment).toHaveBeenCalledWith( - { hi: "hi" }, - 1228 + expect(returnService.receiveReturn).toHaveBeenCalledTimes(1) + expect(returnService.receiveReturn).toHaveBeenCalledWith( + IdMap.getId("good"), + items, + undefined, + false + ) + + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith("item_1", { + returned_quantity: 10, + }) + + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...order, + fulfillment_status: "returned", + }) + + expect(paymentProviderService.refundPayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.refundPayment).toHaveBeenCalledWith( + order.payments, + 100, + "return" ) }) it("return with custom refund", async () => { - await orderService.receiveReturn( - IdMap.getId("returned-order"), - IdMap.getId("return"), - [ - { - item_id: IdMap.getId("existingLine"), - quantity: 10, - }, - ], - 102 - ) - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("returned-order") }, + const items = [ { - $push: { - refunds: { - amount: 102, - }, - }, - $set: { - items: [ - { - _id: IdMap.getId("existingLine"), - content: { - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - }, - description: "This is a new line", - quantity: 10, - returned_quantity: 10, - thumbnail: "test-img-yeah.com/thumb", - title: "merge line", - returned: true, - }, - ], - returns: [ - { - documents: ["doc1234"], - _id: IdMap.getId("return"), - status: "received", - shipping_method: { - _id: IdMap.getId("return-shipping"), - is_return: true, - name: "Return Shipping", - region_id: IdMap.getId("region-france"), - profile_id: IdMap.getId("default-profile"), - data: { - id: "return_shipment", - }, - price: 2, - provider_id: "default_provider", - }, - shipping_data: { - id: "return_shipment", - shipped: true, - }, - items: [ - { - item_id: IdMap.getId("existingLine"), - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - is_requested: true, - is_registered: true, - quantity: 10, - requested_quantity: 10, - }, - ], - refund_amount: 102, - }, - ], - fulfillment_status: "returned", - }, - } + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ] + await orderService.receiveReturn( + IdMap.getId("order"), + IdMap.getId("good"), + items, + 102 ) - expect(DefaultProviderMock.refundPayment).toHaveBeenCalledTimes(1) - expect(DefaultProviderMock.refundPayment).toHaveBeenCalledWith( - { hi: "hi" }, - 102 + expect(returnService.receiveReturn).toHaveBeenCalledTimes(1) + expect(returnService.receiveReturn).toHaveBeenCalledWith( + IdMap.getId("good"), + items, + 102, + false ) }) it("calls order model functions and sets partially_returned", async () => { + const items = [ + { + item_id: IdMap.getId("existingLine"), + quantity: 2, + }, + ] await orderService.receiveReturn( - IdMap.getId("order-refund"), - IdMap.getId("return"), - [ - { - item_id: IdMap.getId("existingLine"), - quantity: 2, - }, - ] + IdMap.getId("order"), + IdMap.getId("good"), + items ) - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("order-refund") }, - { - $push: { - refunds: { - amount: 246, - }, - }, - $set: { - returns: [ - { - _id: IdMap.getId("return"), - status: "received", - shipping_method: { - _id: IdMap.getId("return-shipping"), - is_return: true, - name: "Return Shipping", - region_id: IdMap.getId("region-france"), - profile_id: IdMap.getId("default-profile"), - data: { - id: "return_shipment", - }, - price: 2, - provider_id: "default_provider", - }, - documents: ["doc1234"], - shipping_data: { - id: "return_shipment", - shipped: true, - }, - items: [ - { - item_id: IdMap.getId("existingLine"), - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - is_requested: true, - is_registered: true, - requested_quantity: 2, - quantity: 2, - metadata: {}, - }, - ], - refund_amount: 246, - }, - ], - items: [ - { - _id: IdMap.getId("existingLine"), - content: { - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - unit_price: 100, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - }, - description: "This is a new line", - quantity: 10, - returned: false, - returned_quantity: 2, - thumbnail: "test-img-yeah.com/thumb", - title: "merge line", - }, - { - _id: IdMap.getId("existingLine2"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - quantity: 10, - returned_quantity: 0, - metadata: {}, - }, - ], - fulfillment_status: "partially_returned", - }, - } - ) + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...order, + fulfillment_status: "partially_returned", + }) }) it("sets requires_action on additional items", async () => { await orderService.receiveReturn( - IdMap.getId("order-refund"), - IdMap.getId("return"), + IdMap.getId("order"), + IdMap.getId("action"), [ - { - item_id: IdMap.getId("existingLine"), - quantity: 2, - }, { item_id: IdMap.getId("existingLine2"), quantity: 2, @@ -910,99 +1007,48 @@ describe("OrderService", () => { ] ) - const originalReturn = orders.orderToRefund.returns[0] - const toSet = { - ...originalReturn, - status: "requires_action", - items: [ - ...originalReturn.items.map((i, index) => ({ - ...i, - requested_quantity: i.quantity, - is_requested: index === 0, - is_registered: true, - })), - { - item_id: IdMap.getId("existingLine2"), - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, - is_requested: false, - is_registered: true, - quantity: 2, - metadata: {}, - }, - ], - } - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("order-refund"), "returns._id": originalReturn._id }, - { - $set: { - "returns.$": toSet, - }, - } - ) - }) - - it("sets requires_action on unmatcing quantities", async () => { - await orderService.receiveReturn( - IdMap.getId("order-refund"), - IdMap.getId("return"), - [ - { - item_id: IdMap.getId("existingLine"), - quantity: 1, - }, - ] - ) - - const originalReturn = orders.orderToRefund.returns[0] - const toSet = { - ...originalReturn, - status: "requires_action", - items: originalReturn.items.map(i => ({ - ...i, - requested_quantity: i.quantity, - quantity: 1, - is_requested: false, - is_registered: true, - })), - } - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("order-refund"), "returns._id": originalReturn._id }, - { - $set: { - "returns.$": toSet, - }, - } - ) + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...order, + fulfillment_status: "requires_action", + }) }) }) describe("requestReturn", () => { - const returnService = new ReturnService({ - totalsService: TotalsServiceMock, - shippingOptionService: ShippingOptionServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, + const order = { + items: [ + { + id: "item_1", + quantity: 10, + returned_quantity: 0, + }, + ], + payments: [{ id: "payment_test" }], + } + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + default: + return Promise.resolve(order) + } + }, }) + + const returnService = { + create: jest.fn(() => Promise.resolve({ id: "ret" })), + fulfill: jest.fn(() => Promise.resolve({ id: "ret" })), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ - orderModel: OrderModelMock, + manager: MockManager, + orderRepository: orderRepo, + totalsService, returnService, - shippingOptionService: ShippingOptionServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - paymentProviderService: PaymentProviderServiceMock, - totalsService: TotalsServiceMock, - eventBusService: EventBusServiceMock, + eventBusService, }) beforeEach(async () => { @@ -1021,124 +1067,95 @@ describe("OrderService", () => { price: 2, } await orderService.requestReturn( - IdMap.getId("processed-order"), + "processed-order", items, shipping_method ) - expect(FulfillmentProviderMock.createReturn).toHaveBeenCalledTimes(1) - expect(FulfillmentProviderMock.createReturn).toHaveBeenCalledWith( + expect(returnService.create).toHaveBeenCalledTimes(1) + expect(returnService.create).toHaveBeenCalledWith( { - id: "return_shipment", + items, + shipping_method, + refund_amount: undefined, + order_id: "processed-order", }, - [ - { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - returned_quantity: 0, - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - quantity: 10, - }, - ], - orders.processedOrder + order ) - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("processed-order") }, - { - $push: { - returns: { - shipping_method: { - _id: IdMap.getId("return-shipping"), - is_return: true, - name: "Return Shipping", - region_id: IdMap.getId("region-france"), - profile_id: IdMap.getId("default-profile"), - data: { - id: "return_shipment", - }, - price: 2, - provider_id: "default_provider", - }, - shipping_data: { - id: "return_shipment", - shipped: true, - }, - items: [ - { - item_id: IdMap.getId("existingLine"), - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, - is_requested: true, - quantity: 10, - }, - ], - refund_amount: 1228, - }, - }, - } - ) - }) - - it("sets correct shipping method", async () => { - const items = [ - { - item_id: IdMap.getId("existingLine"), - quantity: 10, - }, - ] - await orderService.requestReturn(IdMap.getId("processed-order"), items) - - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect( - OrderModelMock.updateOne.mock.calls[0][1].$push.returns.refund_amount - ).toEqual(1230) - }) - - it("throws if payment is already processed", async () => { - await expect( - orderService.requestReturn(IdMap.getId("fulfilled-order"), []) - ).rejects.toThrow("Can't return an order with payment unprocessed") - }) - - it("throws if return is attempted on unfulfilled order", async () => { - await expect( - orderService.requestReturn(IdMap.getId("not-fulfilled-order"), []) - ).rejects.toThrow("Can't return an unfulfilled or already returned order") + expect(returnService.fulfill).toHaveBeenCalledWith("ret") }) }) describe("createShipment", () => { - const fulfillmentService = new FulfillmentService({ - fulfillmentProviderService: FulfillmentProviderServiceMock, - shippingProfileService: ShippingProfileServiceMock, - totalsService: TotalsServiceMock, + const partialOrder = { + items: [ + { + id: "item_1", + quantity: 2, + fulfilled_quantity: 0, + }, + { + id: "item_2", + quantity: 1, + fulfilled_quantity: 0, + }, + ], + } + + const order = { + items: [ + { + id: "item_1", + quantity: 2, + fulfilled_quantity: 0, + }, + ], + } + + const orderRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("partial"): + return Promise.resolve(partialOrder) + default: + return Promise.resolve(order) + } + }, }) + + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + + const fulfillmentService = { + retrieve: () => Promise.resolve({ order_id: IdMap.getId("test") }), + createShipment: jest + .fn() + .mockImplementation((shipmentId, tracking, meta) => { + return Promise.resolve({ + items: [ + { + item_id: "item_1", + quantity: 2, + }, + ], + }) + }), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ - orderModel: OrderModelMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, + manager: MockManager, + orderRepository: orderRepo, + totalsService, fulfillmentService, - eventBusService: EventBusServiceMock, + lineItemService, + eventBusService, }) beforeEach(async () => { @@ -1147,140 +1164,24 @@ describe("OrderService", () => { it("calls order model functions", async () => { await orderService.createShipment( - IdMap.getId("shippedOrder"), + IdMap.getId("test"), IdMap.getId("fulfillment"), ["1234", "2345"], {} ) - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("shippedOrder"), - "fulfillments._id": IdMap.getId("fulfillment"), - }, - { - $set: { - "fulfillments.$": { - _id: IdMap.getId("fulfillment"), - provider_id: "default_provider", - tracking_numbers: ["1234", "2345"], - data: {}, - items: [ - { - _id: IdMap.getId("existingLine"), - content: { - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - }, - description: "This is a new line", - fulfilled_quantity: 10, - quantity: 10, - thumbnail: "test-img-yeah.com/thumb", - title: "merge line", - }, - ], - shipped_at: expect.anything(), - metadata: {}, - }, - items: [ - { - _id: IdMap.getId("existingLine"), - content: { - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - }, - description: "This is a new line", - shipped: true, - fulfilled_quantity: 10, - shipped_quantity: 10, - quantity: 10, - thumbnail: "test-img-yeah.com/thumb", - title: "merge line", - }, - ], - fulfillment_status: "shipped", - }, - } + expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1) + expect(fulfillmentService.createShipment).toHaveBeenCalledWith( + IdMap.getId("fulfillment"), + ["1234", "2345"], + {} ) - }) - it("throws if order is unprocessed", async () => { - try { - await orderService.archive(IdMap.getId("test-order")) - } catch (error) { - expect(error.message).toEqual("Can't archive an unprocessed order") - } - }) - }) - - describe("registerSwapCreated", () => { - beforeEach(async () => { - jest.clearAllMocks() - }) - const orderModel = { - findOne: jest - .fn() - .mockReturnValue(Promise.resolve({ _id: IdMap.getId("order") })), - updateOne: jest.fn().mockReturnValue(Promise.resolve()), - } - - it("adds a swap to an order", async () => { - const swapService = { - retrieve: jest - .fn() - .mockReturnValue( - Promise.resolve({ _id: "1235", order_id: IdMap.getId("order") }) - ), - } - const orderService = new OrderService({ - swapService, - orderModel, - eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) }, + expect(orderRepo.save).toHaveBeenCalledTimes(1) + expect(orderRepo.save).toHaveBeenCalledWith({ + ...order, + fulfillment_status: "shipped", }) - - const res = orderService.registerSwapCreated(IdMap.getId("order"), "1235") - expect(res).resolves - - await res - expect(orderModel.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("order"), - }, - { - $addToSet: { swaps: "1235" }, - } - ) - }) - - it("fails if order/swap relationship is not satisfied", async () => { - const swapService = { - retrieve: jest - .fn() - .mockReturnValue( - Promise.resolve({ _id: "1235", order_id: IdMap.getId("order_1") }) - ), - } - const orderService = new OrderService({ - swapService, - orderModel, - eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) }, - }) - - const res = orderService.registerSwapCreated(IdMap.getId("order"), "1235") - expect(res).rejects.toThrow("Swap must belong to the given order") }) }) @@ -1288,25 +1189,30 @@ describe("OrderService", () => { beforeEach(async () => { jest.clearAllMocks() }) - const orderModel = { + const orderRepo = MockRepository({ findOne: jest .fn() - .mockReturnValue(Promise.resolve({ _id: IdMap.getId("order") })), - updateOne: jest.fn().mockReturnValue(Promise.resolve()), - } + .mockReturnValue(Promise.resolve({ id: IdMap.getId("order") })), + }) it("fails if order/swap relationship not satisfied", async () => { const swapService = { retrieve: jest .fn() .mockReturnValue( - Promise.resolve({ _id: "1235", order_id: IdMap.getId("order_1") }) + Promise.resolve({ id: "1235", order_id: IdMap.getId("order_1") }) ), + withTransaction: function() { + return this + }, } + const orderService = new OrderService({ + manager: MockManager, + orderRepository: orderRepo, + totalsService, swapService, - orderModel, - eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) }, + eventBusService, }) const res = orderService.registerSwapReceived( @@ -1320,15 +1226,20 @@ describe("OrderService", () => { const swapService = { retrieve: jest.fn().mockReturnValue( Promise.resolve({ - _id: "1235", + id: "1235", order_id: IdMap.getId("order"), - return: { status: "requested" }, + return_order: { status: "requested" }, }) ), + withTransaction: function() { + return this + }, } const orderService = new OrderService({ + manager: MockManager, + orderRepository: orderRepo, swapService, - orderModel, + totalsService, eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) }, }) @@ -1340,102 +1251,138 @@ describe("OrderService", () => { }) it("registers a swap as received", async () => { - const model = { + const orderRepo = MockRepository({ findOne: jest.fn().mockReturnValue( Promise.resolve({ - _id: IdMap.getId("order_123"), + id: IdMap.getId("order_123"), items: [ { - _id: IdMap.getId("1234"), + id: IdMap.getId("1234"), returned_quantity: 0, quantity: 1, }, ], }) ), - updateOne: jest.fn().mockReturnValue(Promise.resolve()), - } + }) + const swapService = { retrieve: jest.fn().mockReturnValue( Promise.resolve({ - _id: "1235", + id: "1235", order_id: IdMap.getId("order_123"), - return: { status: "received" }, - return_items: [{ item_id: IdMap.getId("1234"), quantity: 1 }], + return_order: { + status: "received", + items: [{ item_id: IdMap.getId("1234"), quantity: 1 }], + }, }) ), - } - const orderService = new OrderService({ - swapService, - orderModel: model, - eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) }, - }) - - await orderService.registerSwapReceived(IdMap.getId("order"), "1235") - - expect(model.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("order_123"), + withTransaction: function() { + return this }, - { - $set: { - items: [ - { - _id: IdMap.getId("1234"), - returned_quantity: 1, - returned: true, - quantity: 1, - }, - ], - }, - } - ) - }) - - it("fails if order/swap relationship is not satisfied", async () => { - const swapService = { - retrieve: jest - .fn() - .mockReturnValue( - Promise.resolve({ _id: "1235", order_id: IdMap.getId("order_1") }) - ), } + + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const orderService = new OrderService({ + manager: MockManager, + orderRepository: orderRepo, + totalsService, swapService, - orderModel, - eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) }, + lineItemService, + eventBusService, }) - const res = orderService.registerSwapCreated(IdMap.getId("order"), "1235") - expect(res).rejects.toThrow("Swap must belong to the given order") + await orderService.registerSwapReceived(IdMap.getId("order_123"), "1235") + + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith(IdMap.getId("1234"), { + returned_quantity: 1, + }) }) }) - describe("archive", () => { - const orderService = new OrderService({ - orderModel: OrderModelMock, - }) - + describe("createRefund", () => { beforeEach(async () => { jest.clearAllMocks() }) - it("calls order model functions", async () => { - await orderService.archive(IdMap.getId("processed-order")) + const orderRepo = MockRepository({ + findOne: jest.fn().mockImplementation(q => { + if (q.where.id === IdMap.getId("cannot")) { + return Promise.resolve({ + id: IdMap.getId("order"), + payments: [ + { + id: "payment", + }, + ], + total: 100, + refunded_total: 100, + }) + } - expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(OrderModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("processed-order") }, - { $set: { status: "archived" } } + return Promise.resolve({ + id: IdMap.getId("order"), + payments: [ + { + id: "payment", + }, + ], + total: 100, + refunded_total: 0, + }) + }), + }) + + const paymentProviderService = { + refundPayment: jest + .fn() + .mockImplementation(p => Promise.resolve({ id: "ref" })), + withTransaction: function() { + return this + }, + } + + const orderService = new OrderService({ + manager: MockManager, + orderRepository: orderRepo, + paymentProviderService, + totalsService, + eventBusService, + }) + + it("success", async () => { + await orderService.createRefund( + IdMap.getId("order_123"), + 100, + "discount", + "note" + ) + + expect(paymentProviderService.refundPayment).toHaveBeenCalledTimes(1) + expect(paymentProviderService.refundPayment).toHaveBeenCalledWith( + [{ id: "payment" }], + 100, + "discount", + "note" ) }) - it("throws if order is unprocessed", async () => { - try { - await orderService.archive(IdMap.getId("test-order")) - } catch (error) { - expect(error.message).toEqual("Can't archive an unprocessed order") - } + it("fails when refund is off", async () => { + await expect( + orderService.createRefund( + IdMap.getId("cannot"), + 100, + "discount", + "note" + ) + ).rejects.toThrow("Cannot refund more than the original order amount") }) }) }) diff --git a/packages/medusa/src/services/__tests__/payment-provider.js b/packages/medusa/src/services/__tests__/payment-provider.js index 13b3d9d10c..e071e2fb7f 100644 --- a/packages/medusa/src/services/__tests__/payment-provider.js +++ b/packages/medusa/src/services/__tests__/payment-provider.js @@ -1,8 +1,11 @@ +import { MockManager, MockRepository } from "medusa-test-utils" import PaymentProviderService from "../payment-provider" describe("ProductService", () => { describe("retrieveProvider", () => { const container = { + manager: MockManager, + paymentSessionRepository: MockRepository(), pp_default_provider: "good", } @@ -27,6 +30,8 @@ describe("ProductService", () => { describe("createSession", () => { const createPayment = jest.fn().mockReturnValue(Promise.resolve()) const container = { + manager: MockManager, + paymentSessionRepository: MockRepository(), pp_default_provider: { createPayment, }, @@ -50,6 +55,17 @@ describe("ProductService", () => { const updatePayment = jest.fn().mockReturnValue(Promise.resolve()) const container = { + manager: MockManager, + paymentSessionRepository: MockRepository({ + findOne: () => + Promise.resolve({ + id: "session", + provider_id: "default_provider", + data: { + id: "1234", + }, + }), + }), pp_default_provider: { updatePayment, }, @@ -60,6 +76,7 @@ describe("ProductService", () => { it("successfully creates session", async () => { await providerService.updateSession( { + id: "session", provider_id: "default_provider", data: { id: "1234", diff --git a/packages/medusa/src/services/__tests__/product-variant.js b/packages/medusa/src/services/__tests__/product-variant.js index 36adfcda72..77aa8f4f87 100644 --- a/packages/medusa/src/services/__tests__/product-variant.js +++ b/packages/medusa/src/services/__tests__/product-variant.js @@ -1,390 +1,844 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockRepository, MockManager } from "medusa-test-utils" +import idMap from "medusa-test-utils/dist/id-map" import ProductVariantService from "../product-variant" -import { ProductVariantModelMock } from "../../models/__mocks__/product-variant" -import { ProductServiceMock } from "../__mocks__/product" -import { RegionServiceMock } from "../__mocks__/region" -import { EventBusServiceMock } from "../__mocks__/event-bus" + +const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, +} describe("ProductVariantService", () => { describe("retrieve", () => { - describe("successfully get product variant", () => { - let res - beforeAll(async () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, - }) - - res = await productVariantService.retrieve(IdMap.getId("validId")) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", () => { - expect(ProductVariantModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("validId"), - }) - }) - - it("returns correct variant", () => { - expect(res.title).toEqual("test") - }) + const productVariantRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("batman")) { + return Promise.resolve(undefined) + } + return Promise.resolve({ id: IdMap.getId("ironman") }) + }, + }) + const productVariantService = new ProductVariantService({ + manager: MockManager, + productVariantRepository, }) - describe("query fail", () => { - let res - beforeAll(async () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, - }) + beforeEach(async () => { + jest.clearAllMocks() + }) - await productVariantService - .retrieve(IdMap.getId("failId")) - .catch(err => { - res = err - }) + it("successfully retrieves a product variant", async () => { + const result = await productVariantService.retrieve( + IdMap.getId("ironman") + ) + + expect(productVariantRepository.findOne).toHaveBeenCalledTimes(1) + expect(productVariantRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("ironman") }, }) - afterAll(() => { - jest.clearAllMocks() - }) + expect(result.id).toEqual(IdMap.getId("ironman")) + }) - it("calls model layer findOne", () => { - expect(ProductVariantModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("failId"), - }) - }) - - it("model query throws error", () => { - expect(res.name).toEqual("database_error") - expect(res.message).toEqual("test error") - }) + it("fails on non-existing product variant id", async () => { + try { + await productVariantService.retrieve(IdMap.getId("batman")) + } catch (error) { + expect(error.message).toBe( + `Variant with id: ${IdMap.getId("batman")} was not found` + ) + } }) }) - describe("createDraft", () => { - beforeAll(() => { + + describe("create", () => { + const productVariantRepository = MockRepository({ + findOne: query => { + return Promise.resolve() + }, + }) + + const productRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("ironmans")) { + return Promise.resolve({ + id: IdMap.getId("ironman"), + options: [ + { + id: IdMap.getId("color"), + title: "red", + }, + { + id: IdMap.getId("size"), + title: "size", + }, + ], + variants: [ + { + id: IdMap.getId("v1"), + title: "V1", + options: [ + { + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "blue", + }, + { + id: IdMap.getId("test"), + option_id: IdMap.getId("size"), + value: "large", + }, + ], + }, + ], + }) + } + return Promise.resolve({ + id: IdMap.getId("ironman"), + options: [ + { + id: IdMap.getId("color"), + title: "red", + }, + ], + variants: [ + { + id: IdMap.getId("v1"), + title: "V1", + options: [ + { + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "blue", + }, + ], + }, + ], + }) + }, + }) + + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + productVariantRepository, + productRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, + }) + + it("successfully create product variant", async () => { + await productVariantService.create(IdMap.getId("ironman"), { + id: IdMap.getId("v2"), + options: [ + { + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "red", + }, + ], }) - productVariantService.createDraft({ - title: "Test Prod", - image: "test-image", - options: [], - prices: [ + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + + expect(productVariantRepository.create).toHaveBeenCalledTimes(1) + expect(productVariantRepository.create).toHaveBeenCalledWith({ + id: IdMap.getId("v2"), + product_id: IdMap.getId("ironman"), + options: [ { - currency_code: "usd", - amount: 100, + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "red", }, ], }) }) - it("calls model layer create", () => { - expect(ProductVariantModelMock.create).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.create).toHaveBeenCalledWith({ - title: "Test Prod", - image: "test-image", - options: [], - prices: [ - { - currency_code: "usd", - amount: 100, - }, - ], - published: false, - }) + it("fails if product options and variant options differ in length", async () => { + try { + await productVariantService.create(IdMap.getId("ironmans"), { + id: IdMap.getId("v2"), + options: [ + { + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "red", + }, + ], + }) + } catch (error) { + expect(error.message).toBe( + "Product options length does not match variant options length. Product has 2 and variant has 1." + ) + } + }) + + it("fails if variants options is missing a product option", async () => { + try { + await productVariantService.create(IdMap.getId("ironmans"), { + id: IdMap.getId("v2"), + options: [ + { + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "red", + }, + { + id: IdMap.getId("test"), + option_id: IdMap.getId("material"), + value: "carbon", + }, + ], + }) + } catch (error) { + expect(error.message).toBe( + "Variant options do not contain value for size" + ) + } + }) + + it("fails if new option already exists", async () => { + try { + await productVariantService.create(IdMap.getId("ironmans"), { + id: IdMap.getId("v2"), + options: [ + { + id: IdMap.getId("test"), + option_id: IdMap.getId("color"), + value: "blue", + }, + { + id: IdMap.getId("test"), + option_id: IdMap.getId("size"), + value: "large", + }, + ], + }) + } catch (error) { + expect(error.message).toBe( + "Variant with title V1 with provided options already exists" + ) + } }) }) describe("publishVariant", () => { - beforeAll(() => { - jest.clearAllMocks() - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, - }) - - productVariantService.publish(IdMap.getId("variantId")) + const productVariantRepository = MockRepository({ + findOne: query => Promise.resolve({ id: IdMap.getId("ironman") }), }) - it("calls model layer create", () => { - expect(ProductVariantModelMock.create).toHaveBeenCalledTimes(0) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("variantId") }, - { $set: { published: true } } + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + productVariantRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("sucessfully publishes a product", async () => { + const result = await productVariantService.publish(IdMap.getId("ironman")) + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product-variant.updated", + { + id: IdMap.getId("ironman"), + } ) + + expect(productVariantRepository.save).toHaveBeenCalledTimes(1) + expect(productVariantRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + published: true, + }) + + expect(result).toEqual({ + id: IdMap.getId("ironman"), + published: true, + }) }) }) describe("update", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, + const productVariantRepository = MockRepository({ + findOne: query => Promise.resolve({ id: IdMap.getId("ironman") }), }) - beforeEach(() => { + const moneyAmountRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("some-amount") }), + }) + + const productOptionValueRepository = MockRepository({ + findOne: () => + Promise.resolve({ id: IdMap.getId("some-value"), value: "blue" }), + }) + + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + moneyAmountRepository, + productVariantRepository, + productOptionValueRepository, + }) + + productVariantService.updateOptionValue = jest + .fn() + .mockReturnValue(() => Promise.resolve()) + + productVariantService.setCurrencyPrice = jest + .fn() + .mockReturnValue(() => Promise.resolve()) + + beforeEach(async () => { jest.clearAllMocks() }) - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() + it("successfully updates variant by id", async () => { + await productVariantService.update(IdMap.getId("ironman"), { + title: "new title", + }) - await productVariantService.update(`${id}`, { title: "new title" }) - - expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { title: "new title" } }, - { runValidators: true } - ) - }) - - it("throw error on invalid variant id type", async () => { - try { - await productVariantService.update(19314235, { title: "new title" }) - } catch (err) { - expect(err.message).toEqual( - "The variantId could not be casted to an ObjectId" - ) - } - }) - - it("throws error when trying to update metadata", async () => { - const id = mongoose.Types.ObjectId() - try { - await productVariantService.update(`${id}`, { - metadata: { key: "value" }, - }) - } catch (err) { - expect(err.message).toEqual("Use setMetadata to update metadata fields") - } - }) - }) - - describe("decorate", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, - }) - - const fakeVariant = { - _id: "1234", - title: "test", - image: "test-image", - prices: [ + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product-variant.updated", { - currency_code: "usd", - amount: 100, - }, - ], - metadata: { testKey: "testValue" }, - published: true, - } + id: IdMap.getId("ironman"), + title: "new title", + } + ) - beforeEach(() => { - jest.clearAllMocks() - }) - - it("returns decorated product", async () => { - const decorated = await productVariantService.decorate(fakeVariant, []) - expect(decorated).toEqual({ - _id: "1234", - metadata: { testKey: "testValue" }, + expect(productVariantRepository.save).toHaveBeenCalledTimes(1) + expect(productVariantRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + title: "new title", }) }) - it("returns decorated product with handle", async () => { - const decorated = await productVariantService.decorate(fakeVariant, [ - "prices", - ]) - expect(decorated).toEqual({ - _id: "1234", - metadata: { testKey: "testValue" }, + it("successfully updates variant", async () => { + await productVariantService.update( + { id: IdMap.getId("ironman"), title: "new title" }, + { + title: "new title", + } + ) + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product-variant.updated", + { + id: IdMap.getId("ironman"), + title: "new title", + } + ) + + expect(productVariantRepository.save).toHaveBeenCalledTimes(1) + expect(productVariantRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + title: "new title", + }) + }) + + it("throws if provided variant is missing an id", async () => { + try { + await productVariantService.update( + { title: "new title" }, + { + title: "new title", + } + ) + } catch (error) { + expect(error.message).toBe("Variant id missing") + } + }) + + it("successfully updates variant metadata", async () => { + await productVariantService.update(IdMap.getId("ironman"), { + title: "new title", + metadata: { + testing: "this", + }, + }) + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product-variant.updated", + { + id: IdMap.getId("ironman"), + title: "new title", + metadata: { + testing: "this", + }, + } + ) + + expect(productVariantRepository.save).toHaveBeenCalledTimes(1) + expect(productVariantRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + title: "new title", + metadata: { + testing: "this", + }, + }) + }) + + it("successfully updates variant prices", async () => { + await productVariantService.update(IdMap.getId("ironman"), { + title: "new title", prices: [ { - currency_code: "usd", - amount: 100, + currency_code: "dkk", + amount: 1000, + sale_amount: 750, }, ], }) + + expect(productVariantService.setCurrencyPrice).toHaveBeenCalledTimes(1) + expect(productVariantService.setCurrencyPrice).toHaveBeenCalledWith( + IdMap.getId("ironman"), + { + currency_code: "dkk", + amount: 1000, + sale_amount: 750, + } + ) + + expect(productVariantRepository.save).toHaveBeenCalledTimes(1) + }) + + it("successfully updates variant options", async () => { + await productVariantService.update(IdMap.getId("ironman"), { + title: "new title", + options: [ + { + option_id: IdMap.getId("color"), + value: "red", + }, + ], + }) + + expect(productVariantService.updateOptionValue).toHaveBeenCalledTimes(1) + expect(productVariantService.updateOptionValue).toHaveBeenCalledWith( + IdMap.getId("ironman"), + IdMap.getId("color"), + "red" + ) + + expect(productVariantRepository.save).toHaveBeenCalledTimes(1) }) }) - describe("setMetadata", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, + describe("setCurrencyPrice", () => { + const productVariantRepository = MockRepository({ + findOne: query => Promise.resolve({ id: IdMap.getId("ironman") }), }) - beforeEach(() => { + const moneyAmountRepository = MockRepository({ + findOne: query => { + if (query.where.currency_code === "usd") { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: IdMap.getId("dkk"), + variant_id: IdMap.getId("ironman"), + currency_code: "dkk", + }) + }, + }) + + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + moneyAmountRepository, + productVariantRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await productVariantService.setMetadata( - `${id}`, - "metadata", - "testMetadata" - ) + it("successfully creates a price if none exist with given currency", async () => { + await productVariantService.setCurrencyPrice(IdMap.getId("ironman"), { + currency_code: "usd", + amount: 100, + }) - expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } - ) + expect(moneyAmountRepository.create).toHaveBeenCalledTimes(1) + expect(moneyAmountRepository.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("ironman"), + currency_code: "usd", + amount: 100, + }) + + expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1) }) - it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() + it("successfully updates a non-regional price if currency exists", async () => { + await productVariantService.setCurrencyPrice(IdMap.getId("ironman"), { + currency_code: "dkk", + amount: 1000, + }) + expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0) + + expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1) + expect(moneyAmountRepository.save).toHaveBeenCalledWith({ + variant_id: IdMap.getId("ironman"), + id: IdMap.getId("dkk"), + currency_code: "dkk", + amount: 1000, + sale_amount: undefined, + }) + }) + }) + + describe("getRegionPrice", () => { + const regionService = { + retrieve: function() { + return Promise.resolve({ + id: IdMap.getId("california"), + name: "California", + }) + }, + withTransaction: function() { + return this + }, + } + const moneyAmountRepository = MockRepository({ + findOne: query => { + if (query.where.variant_id === IdMap.getId("ironmanv2")) { + return Promise.resolve(undefined) + } + if (query.where.variant_id === IdMap.getId("ironman-sale")) { + return Promise.resolve({ + id: IdMap.getId("dkk"), + variant_id: IdMap.getId("ironman-sale"), + currency_code: "dkk", + region_id: IdMap.getId("california"), + amount: 1000, + sale_amount: 750, + }) + } + return Promise.resolve({ + id: IdMap.getId("dkk"), + variant_id: IdMap.getId("ironman"), + currency_code: "dkk", + region_id: IdMap.getId("california"), + amount: 1000, + }) + }, + }) + + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + regionService, + moneyAmountRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully retrieves region price", async () => { + const result = await productVariantService.getRegionPrice( + IdMap.getId("ironman"), + IdMap.getId("california") + ) + + expect(result).toBe(1000) + }) + + it("successfully retrieves region sale price", async () => { + const result = await productVariantService.getRegionPrice( + IdMap.getId("ironman-sale"), + IdMap.getId("california") + ) + + expect(result).toBe(750) + }) + + it("fails if no price is found", async () => { try { - await productVariantService.setMetadata(`${id}`, 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "Key type is invalid. Metadata keys must be strings" + await productVariantService.getRegionPrice( + IdMap.getId("ironmanv2"), + IdMap.getId("california") + ) + } catch (error) { + expect(error.message).toBe( + "A price for region: California could not be found" ) } }) + }) - it("throws error on invalid variantId type", async () => { + describe("setRegionPrice", () => { + const moneyAmountRepository = MockRepository({ + findOne: query => { + if (query.where.region_id === IdMap.getId("cali")) { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: IdMap.getId("dkk"), + variant_id: IdMap.getId("ironman"), + currency_code: "dkk", + amount: 750, + sale_amount: 500, + }) + }, + }) + + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + moneyAmountRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully creates a price if none exist with given region id", async () => { + await productVariantService.setRegionPrice(IdMap.getId("ironman"), { + currency_code: "usd", + amount: 100, + region_id: IdMap.getId("cali"), + }) + + expect(moneyAmountRepository.create).toHaveBeenCalledTimes(1) + expect(moneyAmountRepository.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("ironman"), + currency_code: "usd", + region_id: IdMap.getId("cali"), + amount: 100, + }) + + expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1) + }) + + it("successfully updates a price", async () => { + await productVariantService.setRegionPrice(IdMap.getId("ironman"), { + currency_code: "dkk", + amount: 750, + sale_amount: 500, + }) + + expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0) + + expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1) + expect(moneyAmountRepository.save).toHaveBeenCalledWith({ + variant_id: IdMap.getId("ironman"), + id: IdMap.getId("dkk"), + currency_code: "dkk", + amount: 750, + sale_amount: 500, + }) + }) + }) + + describe("updateOptionValue", () => { + const productOptionValueRepository = MockRepository({ + findOne: query => { + if (query.where.variant_id === IdMap.getId("jibberish")) { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: IdMap.getId("red"), + option_id: IdMap.getId("color"), + value: "red", + }) + }, + }) + + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + productOptionValueRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully updates", async () => { + await productVariantService.updateOptionValue( + IdMap.getId("ironman"), + IdMap.getId("color"), + "more red" + ) + + expect(productOptionValueRepository.save).toHaveBeenCalledTimes(1) + expect(productOptionValueRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("red"), + option_id: IdMap.getId("color"), + value: "more red", + }) + }) + + it("fails if product option value does not exist", async () => { try { - await productVariantService.setMetadata("fakeVariantId", 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "The variantId could not be casted to an ObjectId" + await productVariantService.updateOptionValue( + IdMap.getId("jibberish"), + IdMap.getId("color"), + "red" ) + } catch (error) { + expect(error.message).toBe("Product option value not found") } }) }) describe("addOptionValue", () => { + const productOptionValueRepository = MockRepository({}) + const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - productService: ProductServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + eventBusService, + productOptionValueRepository, }) - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks() }) - it("it successfully adds option value", async () => { + it("successfully add an option value", async () => { await productVariantService.addOptionValue( - IdMap.getId("testVariant"), - IdMap.getId("testOptionId"), - "testValue" + IdMap.getId("ironman"), + IdMap.getId("color"), + "black" ) - expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("testVariant") }, - { - $push: { - options: { - option_id: IdMap.getId("testOptionId"), - value: "testValue", - }, - }, - } - ) - }) + expect(productOptionValueRepository.create).toHaveBeenCalledTimes(1) + expect(productOptionValueRepository.create).toHaveBeenCalledWith({ + variant_id: IdMap.getId("ironman"), + option_id: IdMap.getId("color"), + value: "black", + }) - it("it successfully casts numeric option value to string", async () => { - await productVariantService.addOptionValue( - IdMap.getId("testVariant"), - IdMap.getId("testOptionId"), - 1234 - ) - - expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("testVariant") }, - { - $push: { - options: { - option_id: IdMap.getId("testOptionId"), - value: "1234", - }, - }, - } - ) - }) - - it("throw error if option value is not string", async () => { - try { - await productVariantService.addOptionValue( - IdMap.getId("testVariant"), - IdMap.getId("testOptionId"), - {} - ) - } catch (err) { - expect(err.message).toEqual( - `Option value is not of type string or number` - ) - } + expect(productOptionValueRepository.save).toBeCalledTimes(1) }) }) describe("deleteOptionValue", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - productService: ProductServiceMock, - eventBusService: EventBusServiceMock, + const productOptionValueRepository = MockRepository({ + findOne: query => { + if (query.where.option_id === IdMap.getId("size")) { + return Promise.resolve(undefined) + } + return Promise.resolve({ + variant_id: IdMap.getId("ironman"), + option_id: IdMap.getId("color"), + id: IdMap.getId("test"), + }) + }, }) - beforeEach(() => { + const productVariantService = new ProductVariantService({ + manager: MockManager, + eventBusService, + productOptionValueRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully deletes option value from variant", async () => { await productVariantService.deleteOptionValue( - IdMap.getId("testVariant"), - IdMap.getId("testing") + IdMap.getId("ironman"), + IdMap.getId("color") ) - expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("testVariant") }, - { $pull: { options: { option_id: IdMap.getId("testing") } } } + expect(productOptionValueRepository.softRemove).toBeCalledTimes(1) + expect(productOptionValueRepository.softRemove).toBeCalledWith({ + variant_id: IdMap.getId("ironman"), + option_id: IdMap.getId("color"), + id: IdMap.getId("test"), + }) + }) + + it("successfully resolves if product option value does not exist", async () => { + const result = await productVariantService.deleteOptionValue( + IdMap.getId("ironman"), + IdMap.getId("size") ) + + expect(result).toBe(undefined) }) }) describe("delete", () => { + const productVariantRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("ironmanv2")) { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: IdMap.getId("ironman"), + }) + }, + }) + const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + eventBusService, + productVariantRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("deletes all variants and product successfully", async () => { - await productVariantService.delete(IdMap.getId("deleteId")) + it("successfully deletes variant", async () => { + await productVariantService.delete(IdMap.getId("ironman")) - expect(ProductVariantModelMock.deleteOne).toBeCalledTimes(1) - expect(ProductVariantModelMock.deleteOne).toBeCalledWith({ - _id: IdMap.getId("deleteId"), + expect(productVariantRepository.softRemove).toBeCalledTimes(1) + expect(productVariantRepository.softRemove).toBeCalledWith({ + id: IdMap.getId("ironman"), }) }) + + it("successfully resolves if variant does not exist", async () => { + const result = await productVariantService.delete( + IdMap.getId("ironmanv2") + ) + + expect(result).toBe(undefined) + }) }) describe("canCoverQuantity", () => { + const productVariantRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("no-manageable-ironman")) { + return Promise.resolve({ manage_inventory: false }) + } + if (query.where.id === IdMap.getId("backorder-ironman")) { + return Promise.resolve({ allow_backorder: true }) + } + if (query.where.id === IdMap.getId("no-ironman")) { + return Promise.resolve({ + inventory_quantity: 5, + manage_inventory: true, + allow_backorder: false, + }) + } + return Promise.resolve({ + inventory_quantity: 20, + }) + }, + }) + const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + eventBusService, + productVariantRepository, }) beforeEach(() => { @@ -393,7 +847,7 @@ describe("ProductVariantService", () => { it("returns true if there is more inventory than requested", async () => { const res = await productVariantService.canCoverQuantity( - IdMap.getId("inventory-test"), + IdMap.getId("ironman"), 10 ) @@ -402,7 +856,7 @@ describe("ProductVariantService", () => { it("returns true if inventory not managed", async () => { const res = await productVariantService.canCoverQuantity( - IdMap.getId("no-inventory-test"), + IdMap.getId("no-manageable-ironman"), 10 ) @@ -411,7 +865,7 @@ describe("ProductVariantService", () => { it("returns true if backorders allowed", async () => { const res = await productVariantService.canCoverQuantity( - IdMap.getId("backorder-test"), + IdMap.getId("backorder-ironman"), 10 ) @@ -420,255 +874,11 @@ describe("ProductVariantService", () => { it("returns false if insufficient inventory", async () => { const res = await productVariantService.canCoverQuantity( - IdMap.getId("inventory-test"), + IdMap.getId("no-ironman"), 20 ) expect(res).toEqual(false) }) }) - - describe("setCurrencyPrice", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - eventBusService: EventBusServiceMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("creates a prices array if none exist", async () => { - await productVariantService.setCurrencyPrice( - IdMap.getId("no-prices"), - "usd", - 100 - ) - - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("no-prices"), - }, - { - $set: { - prices: [ - { - currency_code: "usd", - amount: 100, - }, - ], - }, - } - ) - }) - - it("updates all eur prices", async () => { - await productVariantService.setCurrencyPrice( - IdMap.getId("eur-prices"), - "eur", - 100 - ) - - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("eur-prices"), - }, - { - $set: { - prices: [ - { - currency_code: "eur", - amount: 100, - }, - { - region_id: IdMap.getId("region-france"), - currency_code: "eur", - amount: 100, - }, - ], - }, - } - ) - }) - - it("creates usd prices", async () => { - await productVariantService.setCurrencyPrice( - IdMap.getId("eur-prices"), - "usd", - 300 - ) - - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("eur-prices"), - }, - { - $set: { - prices: [ - { - currency_code: "eur", - amount: 1000, - }, - { - region_id: IdMap.getId("region-france"), - currency_code: "eur", - amount: 950, - }, - { - currency_code: "usd", - amount: 300, - }, - ], - }, - } - ) - }) - }) - - describe("setRegionPrice", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - regionService: RegionServiceMock, - eventBusService: EventBusServiceMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("creates a prices array if none exist", async () => { - await productVariantService.setCurrencyPrice( - IdMap.getId("no-prices"), - "usd", - 100 - ) - - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("no-prices"), - }, - { - $set: { - prices: [ - { - currency_code: "usd", - amount: 100, - }, - ], - }, - } - ) - }) - - it("updates all eur prices", async () => { - await productVariantService.setCurrencyPrice( - IdMap.getId("eur-prices"), - "eur", - 100 - ) - - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("eur-prices"), - }, - { - $set: { - prices: [ - { - currency_code: "eur", - amount: 100, - }, - { - region_id: IdMap.getId("region-france"), - currency_code: "eur", - amount: 100, - }, - ], - }, - } - ) - }) - - it("creates usd prices", async () => { - await productVariantService.setCurrencyPrice( - IdMap.getId("eur-prices"), - "usd", - 300 - ) - - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("eur-prices"), - }, - { - $set: { - prices: [ - { - currency_code: "eur", - amount: 1000, - }, - { - region_id: IdMap.getId("region-france"), - currency_code: "eur", - amount: 950, - }, - { - currency_code: "usd", - amount: 300, - }, - ], - }, - } - ) - }) - }) - - describe("getRegionPrice", () => { - const productVariantService = new ProductVariantService({ - productVariantModel: ProductVariantModelMock, - regionService: RegionServiceMock, - eventBusService: EventBusServiceMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("gets region specific price", async () => { - const res = await productVariantService.getRegionPrice( - IdMap.getId("eur-prices"), - IdMap.getId("region-france") - ) - - expect(res).toEqual(950) - }) - - it("gets currency default price", async () => { - const res = await productVariantService.getRegionPrice( - IdMap.getId("eur-prices"), - IdMap.getId("region-de") - ) - - expect(res).toEqual(1000) - }) - - it("throws if no region or currency", async () => { - try { - const res = await productVariantService.getRegionPrice( - IdMap.getId("eur-prices"), - IdMap.getId("region-se") - ) - } catch (err) { - expect(err.message).toEqual( - "A price for region: Sweden could not be found" - ) - } - }) - }) }) diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index 355714552f..af45fcfdf8 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -1,710 +1,474 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import ProductService from "../product" -import { ProductModelMock } from "../../models/__mocks__/product" -import { - ProductVariantServiceMock, - variants, -} from "../__mocks__/product-variant" -import { EventBusServiceMock } from "../__mocks__/event-bus" + +const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, +} describe("ProductService", () => { describe("retrieve", () => { - describe("successfully get product", () => { - let res - beforeAll(async () => { - const productService = new ProductService({ - productModel: ProductModelMock, - eventBusService: EventBusServiceMock, - }) - - res = await productService.retrieve(IdMap.getId("validId")) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", () => { - expect(ProductModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ProductModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("validId"), - }) - }) - - it("returns correct product", () => { - expect(res.title).toEqual("test") - }) + const productRepo = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), }) - - describe("query fail", () => { - let res - beforeAll(async () => { - const productService = new ProductService({ - productModel: ProductModelMock, - eventBusService: EventBusServiceMock, - }) - - await productService.retrieve(IdMap.getId("failId")).catch(err => { - res = err - }) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", () => { - expect(ProductModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ProductModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("failId"), - }) - }) - - it("model query throws error", () => { - expect(res.name).toEqual("database_error") - expect(res.message).toEqual("test error") - }) - }) - }) - - describe("createDraft", () => { - beforeAll(() => { - jest.clearAllMocks() - const productService = new ProductService({ - productModel: ProductModelMock, - eventBusService: EventBusServiceMock, - }) - - productService.createDraft({ - title: "Test Prod", - description: "Test Descript", - tags: "Teststst", - handle: "1234", - images: [], - options: [], - variants: [], - metadata: {}, - }) - }) - - it("calls model layer create", () => { - expect(ProductModelMock.create).toHaveBeenCalledTimes(1) - expect(ProductModelMock.create).toHaveBeenCalledWith({ - title: "Test Prod", - description: "Test Descript", - tags: "Teststst", - handle: "1234", - images: [], - options: [], - variants: [], - metadata: {}, - published: false, - }) - }) - }) - - describe("publishProduct", () => { - const productId = mongoose.Types.ObjectId() - - beforeAll(() => { - jest.clearAllMocks() - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, - }) - - productService.publish(IdMap.getId("productId")) - }) - - it("calls model layer create", () => { - expect(ProductModelMock.create).toHaveBeenCalledTimes(0) - expect(ProductModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ProductModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("productId") }, - { $set: { published: true } } - ) - }) - }) - - describe("decorate", () => { const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + productRepository: productRepo, }) - const fakeProduct = { - _id: IdMap.getId("fakeId"), - variants: ["1", "2", "3"], - tags: "testtag1, testtag2", - handle: "test-product", - metadata: { testKey: "testValue" }, - } - - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks() }) - it("returns decorated product", async () => { - const decorated = await productService.decorate( - fakeProduct, - [], - ["variants"] - ) - expect(decorated).toEqual({ - _id: IdMap.getId("fakeId"), - metadata: { testKey: "testValue" }, - variants: [variants.one, variants.two, variants.three], - }) - }) + it("successfully retrieves a product", async () => { + const result = await productService.retrieve(IdMap.getId("ironman")) - it("returns decorated product with handle", async () => { - const decorated = await productService.decorate( - fakeProduct, - ["handle"], - ["variants"] - ) - expect(decorated).toEqual({ - _id: IdMap.getId("fakeId"), - metadata: { testKey: "testValue" }, - handle: "test-product", - variants: [variants.one, variants.two, variants.three], + expect(productRepo.findOne).toHaveBeenCalledTimes(1) + expect(productRepo.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("ironman") }, }) - }) - it("returns decorated product with handle and tags", async () => { - const decorated = await productService.decorate(fakeProduct, [ - "handle", - "tags", - ]) - expect(decorated).toEqual({ - _id: IdMap.getId("fakeId"), - metadata: { testKey: "testValue" }, - tags: "testtag1, testtag2", - handle: "test-product", - }) - }) - - it("returns decorated product with metadata", async () => { - const decorated = await productService.decorate(fakeProduct, []) - expect(decorated).toEqual({ - _id: IdMap.getId("fakeId"), - metadata: { testKey: "testValue" }, - }) + expect(result.id).toEqual(IdMap.getId("ironman")) }) }) - describe("setMetadata", () => { + describe("create", () => { + const productRepository = MockRepository({ + create: () => + Promise.resolve({ id: IdMap.getId("ironman"), title: "Suit" }), + }) const productService = new ProductService({ - productModel: ProductModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + productRepository, + eventBusService, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await productService.setMetadata(`${id}`, "metadata", "testMetadata") + it("successfully create a product", async () => { + const result = await productService.create({ + title: "Suit", + options: [], + }) - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product.created", + expect.any(Object) ) - }) - it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() + expect(productRepository.create).toHaveBeenCalledTimes(1) + expect(productRepository.create).toHaveBeenCalledWith({ + title: "Suit", + options: [], + }) - try { - await productService.setMetadata(`${id}`, 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "Key type is invalid. Metadata keys must be strings" - ) - } - }) + expect(productRepository.save).toHaveBeenCalledTimes(1) - it("throws error on invalid productId type", async () => { - try { - await productService.setMetadata("fakeProductId", 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "The productId could not be casted to an ObjectId" - ) - } + expect(result).toEqual({ + id: IdMap.getId("ironman"), + title: "Suit", + options: [], + }) }) }) describe("update", () => { + const productRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("ironman&co")) { + return Promise.resolve({ + id: IdMap.getId("ironman&co"), + variants: [{ id: IdMap.getId("green"), title: "Green" }], + }) + } + if (query.where.id === "123") { + return undefined + } + return Promise.resolve({ id: IdMap.getId("ironman") }) + }, + }) + + const productVariantRepository = MockRepository() + + const productVariantService = { + withTransaction: function() { + return this + }, + update: () => Promise.resolve(), + } + const productService = new ProductService({ - productModel: ProductModelMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + productRepository, + productVariantService, + productVariantRepository, + eventBusService, }) beforeEach(() => { jest.clearAllMocks() }) - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() + it("successfully updates product metadata", async () => { + await productService.update(IdMap.getId("ironman"), { + metadata: { some_key: "some_value" }, + }) - await productService.update(`${id}`, { title: "new title" }) - - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { title: "new title" } }, - { runValidators: true } + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product.updated", + expect.any(Object) ) + + expect(productRepository.save).toHaveBeenCalledTimes(1) + expect(productRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + metadata: { some_key: "some_value" }, + }) }) - it("throw error on invalid product id type", async () => { + it("successfully updates product variants", async () => { + await productService.update(IdMap.getId("ironman&co"), { + variants: [{ id: IdMap.getId("green"), title: "Greener" }], + }) + + // The update of variants will be tested in product variant test file + // Here we just test, that the function reaches its end when updating + // variants + expect(productRepository.save).toHaveBeenCalledTimes(1) + }) + + it("successfully updates product", async () => { + await productService.update(IdMap.getId("ironman"), { + title: "Full suit", + }) + + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product.updated", + expect.any(Object) + ) + + expect(productRepository.save).toHaveBeenCalledTimes(1) + expect(productRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + title: "Full suit", + }) + }) + + it("throws on non existing product", async () => { try { - await productService.update(19314235, { title: "new title" }) + await productService.update("123", { title: "new title" }) } catch (err) { - expect(err.message).toEqual( - "The productId could not be casted to an ObjectId" - ) + expect(err.message).toEqual("Product with id: 123 was not found") } }) - it("throws error when trying to update metadata", async () => { - const id = mongoose.Types.ObjectId() + it("throws on wrong variant in update", async () => { try { - await productService.update(`${id}`, { metadata: { key: "value" } }) + await productService.update(IdMap.getId("ironman&co"), { + variants: [ + { id: IdMap.getId("yellow") }, + { id: IdMap.getId("green") }, + ], + }) } catch (err) { - expect(err.message).toEqual("Use setMetadata to update metadata fields") + expect(err.message).toEqual( + `Variant with id: ${IdMap.getId( + "yellow" + )} is not associated with this product` + ) } }) }) describe("delete", () => { + const productRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + }) + const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + productRepository, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + it("successfully deletes product", async () => { + await productService.delete(IdMap.getId("ironman")) + + expect(productRepository.softRemove).toBeCalledTimes(1) + expect(productRepository.softRemove).toBeCalledWith({ + id: IdMap.getId("ironman"), + }) + }) + }) + + describe("addOption", () => { + const productRepository = MockRepository({ + findOne: query => + Promise.resolve({ + id: IdMap.getId("ironman"), + options: [{ title: "Color" }], + variants: [{ id: IdMap.getId("green") }], + }), + }) + + const productVariantService = { + withTransaction: function() { + return this + }, + addOptionValue: jest.fn(), + } + + const productOptionRepository = MockRepository({ + create: () => + Promise.resolve({ id: IdMap.getId("Material"), title: "Material" }), + }) + + const productService = new ProductService({ + manager: MockManager, + productRepository, + productOptionRepository, + productVariantService, + eventBusService, }) beforeEach(() => { jest.clearAllMocks() }) - it("deletes all variants and product successfully", async () => { - await productService.delete(IdMap.getId("deleteId")) + it("creates product option", async () => { + await productService.addOption(IdMap.getId("ironman"), "Material") - expect(ProductVariantServiceMock.delete).toBeCalledTimes(2) - expect(ProductVariantServiceMock.delete).toBeCalledWith("1") - expect(ProductVariantServiceMock.delete).toBeCalledWith("2") - - expect(ProductModelMock.deleteOne).toBeCalledTimes(1) - expect(ProductModelMock.deleteOne).toBeCalledWith({ - _id: IdMap.getId("deleteId"), + expect(productOptionRepository.create).toHaveBeenCalledWith({ + title: "Material", + product_id: IdMap.getId("ironman"), }) - }) - }) + expect(productOptionRepository.create).toHaveBeenCalledTimes(1) - describe("createVariant", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, - }) + expect(productOptionRepository.save).toHaveBeenCalledTimes(1) - afterEach(() => { - jest.clearAllMocks() - }) - - it("add variant to product successfilly", async () => { - await productService.createVariant(IdMap.getId("variantProductId"), { - title: "variant1", - options: [ - { - option_id: IdMap.getId("color_id"), - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "160", - }, - ], - }) - - expect(ProductVariantServiceMock.createDraft).toBeCalledTimes(1) - expect(ProductVariantServiceMock.createDraft).toBeCalledWith({ - title: "variant1", - options: [ - { - option_id: IdMap.getId("color_id"), - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "160", - }, - ], - }) - - expect(ProductModelMock.findOne).toBeCalledTimes(2) - expect(ProductModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("variantProductId"), - }) - - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("variantProductId") }, - { $push: { variants: expect.stringMatching(/.*/) } } - ) - }) - - it("add variant to product successfully", async () => { - await productService.createVariant( - IdMap.getId("productWithFourVariants"), - { - title: "variant1", - options: [ - { - option_id: IdMap.getId("color_id"), - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "1600", - }, - ], - } - ) - - expect(ProductVariantServiceMock.createDraft).toBeCalledTimes(1) - expect(ProductVariantServiceMock.createDraft).toBeCalledWith({ - title: "variant1", - options: [ - { - option_id: IdMap.getId("color_id"), - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "1600", - }, - ], - }) - - expect(ProductModelMock.findOne).toBeCalledTimes(2) - expect(ProductModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("productWithFourVariants"), - }) - - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("productWithFourVariants") }, - { $push: { variants: expect.stringMatching(/.*/) } } - ) - }) - - it("throws error if option id is not present in product", async () => { - await expect( - productService.createVariant(IdMap.getId("variantProductId"), { - title: "variant3", - options: [ - { - option_id: "invalid_id", - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "150", - }, - ], - }) - ).rejects.toThrow("Variant options do not contain value for Color") - }) - - it("throws error if product variant options is empty", async () => { - await expect( - productService.createVariant(IdMap.getId("variantProductId"), { - title: "variant3", - options: [], - }) - ).rejects.toThrow( - "Product options length does not match variant options length. Product has 2 and variant has 0." - ) - }) - - it("throws error if product options is empty and product variant contains options", async () => { - await expect( - productService.createVariant(IdMap.getId("emptyVariantProductId"), { - title: "variant1", - options: [ - { - option_id: IdMap.getId("color_id"), - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "160", - }, - ], - }) - ).rejects.toThrow( - "Product options length does not match variant options length. Product has 0 and variant has 2." - ) - }) - - it("throws error if option values of added variant already exists", async () => { - await expect( - productService.createVariant(IdMap.getId("productWithVariants"), { - title: "variant3", - options: [ - { - option_id: IdMap.getId("color_id"), - value: "blue", - }, - { - option_id: IdMap.getId("size_id"), - value: "150", - }, - ], - }) - ).rejects.toThrow("Variant with provided options already exists") - }) - }) - - describe("addOption", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it("creates variant option values and adds option", async () => { - await productService.addOption( - IdMap.getId("productWithVariants"), - "Material" - ) - - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledTimes(3) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - "1", - expect.anything(), - "Default Value" - ) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - "3", - expect.anything(), - "Default Value" - ) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - "4", - expect.anything(), + expect(productVariantService.addOptionValue).toHaveBeenCalledTimes(1) + expect(productVariantService.addOptionValue).toHaveBeenCalledWith( + IdMap.getId("green"), + IdMap.getId("Material"), "Default Value" ) - expect(ProductModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("productWithVariants") }, - { - $push: { - options: { - _id: expect.anything(), - title: "Material", - product_id: IdMap.getId("productWithVariants"), - }, - }, - } + expect(eventBusService.emit).toHaveBeenCalledTimes(1) + expect(eventBusService.emit).toHaveBeenCalledWith( + "product.updated", + expect.any(Object) ) }) - it("cleans up after fail", async () => { + it("throws on duplicate option", async () => { try { await productService.addOption( IdMap.getId("productWithVariantsFail"), - "Material" + "Color" ) } catch (err) { - expect(err) + expect(err.message).toBe( + "An option with the title: Color already exists" + ) } - - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledTimes(3) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - "1", - expect.anything(), - "Default Value" - ) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - "3", - expect.anything(), - "Default Value" - ) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - "4", - expect.anything(), - "Default Value" - ) - - expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledTimes( - 3 - ) - expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith( - "1", - expect.anything() - ) - expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith( - "3", - expect.anything() - ) - expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith( - "4", - expect.anything() - ) }) }) - describe("deleteOption", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + describe("reorderVariants", () => { + const productRepository = MockRepository({ + findOne: query => + Promise.resolve({ + id: IdMap.getId("ironman"), + variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }], + }), }) - afterEach(() => { + const productService = new ProductService({ + manager: MockManager, + productRepository, + eventBusService, + }) + + beforeEach(() => { jest.clearAllMocks() }) - it("deletes an option from a product", async () => { - await productService.deleteOption( - IdMap.getId("productWithVariants"), - IdMap.getId("color_id") - ) + it("reorders variants", async () => { + await productService.reorderVariants(IdMap.getId("ironman"), [ + IdMap.getId("blue"), + IdMap.getId("green"), + ]) - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("productWithVariants") }, - { $pull: { options: { _id: IdMap.getId("color_id") } } } - ) - expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledTimes(3) - expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledWith( - "1", - IdMap.getId("color_id") - ) - expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledWith( - "3", - IdMap.getId("color_id") - ) - expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledWith( - "4", - IdMap.getId("color_id") - ) + expect(productRepository.save).toBeCalledTimes(1) + expect(productRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + variants: [{ id: IdMap.getId("blue") }, { id: IdMap.getId("green") }], + }) }) - it("if option does not exist, do nothing", async () => { - await productService.deleteOption( - IdMap.getId("productWithVariants"), - IdMap.getId("option_id") - ) - - expect(ProductModelMock.updateOne).not.toBeCalled() - }) - - it("throw if variant option values are not equal", async () => { + it("throws if a variant id is not in the products variants", async () => { try { - await productService.deleteOption( - IdMap.getId("productWithFourVariants"), - IdMap.getId("size_id") - ) + await productService.reorderVariants(IdMap.getId("ironman"), [ + IdMap.getId("yellow"), + IdMap.getId("blue"), + ]) } catch (err) { expect(err.message).toEqual( - "To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist. For more info check MEDUSA.com" + `Product has no variant with id: ${IdMap.getId("yellow")}` ) } + }) - expect(ProductModelMock.updateOne).not.toBeCalled() + it("throws if order length and product variant lengths differ", async () => { + try { + await productService.reorderVariants(IdMap.getId("ironman"), [ + IdMap.getId("blue"), + ]) + } catch (err) { + expect(err.message).toEqual( + `Product variants and new variant order differ in length.` + ) + } }) }) - describe("deleteVariant", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + describe("reorderOptions", () => { + const productRepository = MockRepository({ + findOne: query => + Promise.resolve({ + id: IdMap.getId("ironman"), + options: [ + { id: IdMap.getId("material") }, + { id: IdMap.getId("color") }, + ], + }), }) - afterEach(() => { + const productService = new ProductService({ + manager: MockManager, + productRepository, + eventBusService, + }) + + beforeEach(() => { jest.clearAllMocks() }) - it("removes variant from product", async () => { - await productService.deleteVariant( - IdMap.getId("productWithVariants"), - "1" - ) + it("reorders options", async () => { + await productService.reorderOptions(IdMap.getId("ironman"), [ + IdMap.getId("color"), + IdMap.getId("material"), + ]) - expect(ProductVariantServiceMock.delete).toBeCalledTimes(1) - expect(ProductVariantServiceMock.delete).toBeCalledWith("1") + expect(productRepository.save).toBeCalledTimes(1) + expect(productRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + options: [ + { id: IdMap.getId("color") }, + { id: IdMap.getId("material") }, + ], + }) + }) - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("productWithVariants") }, - { $pull: { variants: "1" } } - ) + it("throws if one option id is not in the product options", async () => { + try { + await productService.reorderOptions(IdMap.getId("ironman"), [ + IdMap.getId("packaging"), + IdMap.getId("material"), + ]) + } catch (err) { + expect(err.message).toEqual( + `Product has no option with id: ${IdMap.getId("packaging")}` + ) + } + }) + + it("throws if order length and product option lengths differ", async () => { + try { + await productService.reorderOptions(IdMap.getId("ironman"), [ + IdMap.getId("size"), + IdMap.getId("color"), + IdMap.getId("material"), + ]) + } catch (err) { + expect(err.message).toEqual( + `Product options and new options order differ in length.` + ) + } }) }) describe("updateOption", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + const productRepository = MockRepository({ + findOne: query => + Promise.resolve({ + id: IdMap.getId("ironman"), + options: [ + { id: IdMap.getId("material"), title: "Material" }, + { id: IdMap.getId("color"), title: "Color" }, + ], + }), }) - afterEach(() => { + const productOptionRepository = MockRepository({ + findOne: () => + Promise.resolve({ id: IdMap.getId("color"), title: "Color" }), + }) + + const productService = new ProductService({ + manager: MockManager, + productRepository, + productOptionRepository, + eventBusService, + }) + + beforeEach(() => { jest.clearAllMocks() }) - it("updates title", async () => { + it("updates option title", async () => { await productService.updateOption( - IdMap.getId("productWithVariants"), - IdMap.getId("color_id"), + IdMap.getId("ironman"), + IdMap.getId("color"), { - title: "Shoe Color", + title: "Suit color", } ) - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { - _id: IdMap.getId("productWithVariants"), - "options._id": IdMap.getId("color_id"), - }, - { $set: { "options.$.title": "Shoe Color" } } - ) + expect(productOptionRepository.save).toBeCalledTimes(1) + expect(productOptionRepository.save).toBeCalledWith({ + id: IdMap.getId("color"), + title: "Suit color", + }) }) it("throws if option title exists", async () => { try { await productService.updateOption( - IdMap.getId("productWithVariants"), - IdMap.getId("color_id"), + IdMap.getId("ironman"), + IdMap.getId("color"), { - title: "Size", + title: "Color", } ) } catch (err) { - expect(err.message).toEqual("An option with title Size already exists") + expect(err.message).toEqual("An option with title Color already exists") } }) it("throws if option doesn't exist", async () => { try { await productService.updateOption( - IdMap.getId("productWithVariants"), + IdMap.getId("ironman"), IdMap.getId("material"), { title: "Size", @@ -718,181 +482,99 @@ describe("ProductService", () => { }) }) - describe("reorderOptions", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + describe("deleteOption", () => { + const productRepository = MockRepository({ + findOne: query => + Promise.resolve({ + id: IdMap.getId("ironman"), + variants: [ + { + id: IdMap.getId("red"), + options: [ + { + id: IdMap.getId("option1"), + option_id: IdMap.getId("color"), + value: "red", + }, + { + id: IdMap.getId("option2"), + option_id: IdMap.getId("size"), + value: "large", + }, + ], + }, + { + id: IdMap.getId("red2"), + options: [ + { + id: IdMap.getId("option2"), + option_id: IdMap.getId("color"), + value: "red", + }, + { + id: IdMap.getId("option1"), + option_id: IdMap.getId("size"), + value: "small", + }, + ], + }, + ], + }), }) - afterEach(() => { - jest.clearAllMocks() - }) - - it("reorders options", async () => { - await productService.reorderOptions(IdMap.getId("productWithVariants"), [ - IdMap.getId("size_id"), - IdMap.getId("color_id"), - ]) - - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { - _id: IdMap.getId("productWithVariants"), - }, - { - $set: { - options: [ - { - _id: IdMap.getId("size_id"), - title: "Size", - }, - { - _id: IdMap.getId("color_id"), - title: "Color", - }, - ], - }, + const productOptionRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("material")) { + return undefined } - ) + return Promise.resolve({ id: IdMap.getId("color"), title: "Color" }) + }, }) - it("throws if one option id is not in the product options", async () => { - try { - await productService.reorderOptions( - IdMap.getId("productWithVariants"), - [IdMap.getId("size_id"), IdMap.getId("material")] - ) - } catch (err) { - expect(err.message).toEqual( - `Product has no option with id: ${IdMap.getId("material")}` - ) - } - }) - - it("throws if order length and product option lengths differ", async () => { - try { - await productService.reorderOptions( - IdMap.getId("productWithVariants"), - [ - IdMap.getId("size_id"), - IdMap.getId("color_id"), - IdMap.getId("material"), - ] - ) - } catch (err) { - expect(err.message).toEqual( - `Product options and new options order differ in length. To delete or add options use removeOption or addOption` - ) - } - }) - }) - - describe("reorderVariants", () => { const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, + manager: MockManager, + productRepository, + productOptionRepository, + eventBusService, }) - afterEach(() => { + beforeEach(() => { jest.clearAllMocks() }) - it("reorders variants", async () => { - await productService.reorderVariants(IdMap.getId("productWithVariants"), [ - "3", - "4", - "1", - ]) - - expect(ProductModelMock.updateOne).toBeCalledTimes(1) - expect(ProductModelMock.updateOne).toBeCalledWith( - { - _id: IdMap.getId("productWithVariants"), - }, - { - $set: { - variants: ["3", "4", "1"], - }, - } + it("deletes an option from a product", async () => { + await productService.deleteOption( + IdMap.getId("ironman"), + IdMap.getId("color") ) + + expect(productOptionRepository.softRemove).toBeCalledTimes(1) + expect(productOptionRepository.softRemove).toBeCalledWith({ + id: IdMap.getId("color"), + title: "Color", + }) }) - it("throws if a variant id is not in the products variants", async () => { - try { - await productService.reorderVariants( - IdMap.getId("productWithVariants"), - ["1", "2", "3"] - ) - } catch (err) { - expect(err.message).toEqual(`Product has no variant with id: 2`) - } + it("resolve if product option does not exist", async () => { + await productService.deleteOption( + IdMap.getId("ironman"), + IdMap.getId("material") + ) + + expect(productOptionRepository.save).not.toBeCalled() }) - it("throws if order length and product variant lengths differ", async () => { + it("throw if variant option values are not equal", async () => { try { - await productService.reorderVariants( - IdMap.getId("productWithVariants"), - ["1", "2", "3", "4"] + await productService.deleteOption( + IdMap.getId("ironman"), + IdMap.getId("color") ) - } catch (err) { - expect(err.message).toEqual( - `Product variants and new variant order differ in length. To delete or add variants use removeVariant or addVariant` + } catch (error) { + expect(error.message).toBe( + "To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist." ) } }) }) - - describe("updateOptionValue", () => { - const productService = new ProductService({ - productModel: ProductModelMock, - productVariantService: ProductVariantServiceMock, - eventBusService: EventBusServiceMock, - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it("successfully updates an option value", async () => { - await productService.updateOptionValue( - IdMap.getId("productWithVariants"), - "1", - IdMap.getId("color_id"), - "Blue" - ) - - expect(ProductVariantServiceMock.updateOptionValue).toBeCalledTimes(1) - expect(ProductVariantServiceMock.updateOptionValue).toBeCalledWith( - "1", - IdMap.getId("color_id"), - "Blue" - ) - }) - - it("throws product-variant relationship isn't valid", async () => { - await expect( - productService.updateOptionValue( - IdMap.getId("productWithFourVariants"), - "invalid_variant", - IdMap.getId("color_id"), - "Blue" - ) - ).rejects.toThrow("The variant could not be found in the product") - }) - - it("throws if combination exists", async () => { - await expect( - productService.updateOptionValue( - IdMap.getId("productWithFourVariants"), - "1", - IdMap.getId("color_id"), - "black" - ) - ).rejects.toThrow( - "A variant with the given option value combination already exist" - ) - }) - }) }) diff --git a/packages/medusa/src/services/__tests__/region.js b/packages/medusa/src/services/__tests__/region.js index 3c805353bb..a7ad8acf14 100644 --- a/packages/medusa/src/services/__tests__/region.js +++ b/packages/medusa/src/services/__tests__/region.js @@ -1,521 +1,681 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import RegionService from "../region" -import { RegionModelMock } from "../../models/__mocks__/region" -import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" -import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider" -import { StoreServiceMock } from "../__mocks__/store" describe("RegionService", () => { describe("create", () => { - beforeEach(() => { + 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", + 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 = { + retrieve: () => { + return { + id: IdMap.getId("test-store"), + currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], + } + }, + } + + const regionService = new RegionService({ + manager: MockManager, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, + regionRepository, + countryRepository, + storeService, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully creates a new region", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - storeService: StoreServiceMock, - }) - await regionService.create({ - name: "Denmark", - currency_code: "dkk", + name: "World", + currency_code: "USD", tax_rate: 0.25, - countries: ["DK"], + countries: ["US"], }) - expect(RegionModelMock.create).toHaveBeenCalledTimes(1) - expect(RegionModelMock.create).toHaveBeenCalledWith({ - name: "Denmark", - currency_code: "DKK", + expect(regionRepository.create).toHaveBeenCalledTimes(1) + expect(regionRepository.create).toHaveBeenCalledWith({ + name: "World", + currency_code: "usd", + currency: { + code: "usd", + }, tax_rate: 0.25, - countries: ["DK"], + countries: [{ id: IdMap.getId("test-country"), name: "World" }], }) }) - it("create with payment/fulfillment providers", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - storeService: StoreServiceMock, - }) + it("throws if country already is in region", async () => { + try { + await regionService.create({ + name: "World", + currency_code: "EUR", + tax_rate: 0.25, + countries: ["DK"], + }) + } catch (error) { + expect(error.message).toBe( + "Denmark already exists in Denmark, delete it in that region before adding it" + ) + } + }) + it("successfully creates with payment- and fulfillmentproviders", async () => { await regionService.create({ - name: "Denmark", - currency_code: "dkk", + name: "World", + currency_code: "usd", tax_rate: 0.25, - countries: ["DK"], + countries: ["US"], payment_providers: ["default_provider"], fulfillment_providers: ["default_provider"], }) - expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledTimes( - 1 - ) - expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledWith( - "default_provider" - ) + expect(ppRepository.findOne).toHaveBeenCalledTimes(1) + expect(fpRepository.findOne).toHaveBeenCalledTimes(1) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledTimes(1) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledWith("default_provider") - - expect(RegionModelMock.create).toHaveBeenCalledTimes(1) - expect(RegionModelMock.create).toHaveBeenCalledWith({ - name: "Denmark", - currency_code: "DKK", + expect(regionRepository.create).toHaveBeenCalledTimes(1) + expect(regionRepository.create).toHaveBeenCalledWith({ + name: "World", tax_rate: 0.25, - countries: ["DK"], - payment_providers: ["default_provider"], - fulfillment_providers: ["default_provider"], + currency_code: "usd", + currency: { + code: "usd", + }, + countries: [{ id: IdMap.getId("test-country"), name: "World" }], + payment_providers: [{ id: "default_provider" }], + fulfillment_providers: [{ id: "default_provider" }], }) }) + + it("throws on invalid payment provider", async () => { + try { + await regionService.create({ + name: "World", + currency_code: "EUR", + tax_rate: 0.25, + countries: ["US"], + payment_providers: ["should_fail"], + }) + } catch (error) { + expect(error.message).toBe("Payment provider not found") + } + }) + + it("throws on invalid fulfillment provider", async () => { + try { + await regionService.create({ + name: "World", + currency_code: "EUR", + tax_rate: 0.25, + countries: ["US"], + fulfillment_providers: ["should_fail"], + }) + } catch (error) { + expect(error.message).toBe("Fulfillment provider not found") + } + }) }) describe("retrieve", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("region") }), + }) + + const regionService = new RegionService({ + manager: MockManager, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully retrieves a region", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - }) + await regionService.retrieve(IdMap.getId("region")) - await regionService.retrieve(IdMap.getId("region-se")) - - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(regionRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("region") }, }) }) }) describe("validateFields_", () => { - beforeEach(() => { + 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", + region_id: IdMap.getId("dk-reg"), + }) + } + return Promise.resolve({ + id: IdMap.getId("test-country"), + name: "World", + }) + }, + }) + + const storeService = { + retrieve: () => { + return { + id: IdMap.getId("test-store"), + currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], + } + }, + } + + const regionService = new RegionService({ + manager: MockManager, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + regionRepository, + countryRepository, + storeService, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("throws on invalid currency code", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - storeService: StoreServiceMock, - }) - - await expect( - regionService.validateFields_({ currency_code: "1cw" }) - ).rejects.toThrow("Invalid currency code") - }) - it("throws on invalid country code", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - await expect( regionService.validateFields_({ countries: ["ddd"] }) ).rejects.toThrow("Invalid country code") }) it("throws on in use country code", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - await expect( - regionService.validateFields_({ countries: ["se"] }) + regionService.validateFields_({ countries: ["DK"] }) ).rejects.toThrow( - "Sweden already exists in Sweden, delete it in that region before adding it" + "Denmark already exists in Denmark, delete it in that region before adding it" ) }) - it("throws on invalid tax_rate", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - - await expect( - regionService.validateFields_({ tax_rate: 12 }) - ).rejects.toThrow("The tax_rate must be between 0 and 1") - }) - - it("throws on metadata", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - - await expect( - regionService.validateFields_({ metadata: { key: "Valie" } }) - ).rejects.toThrow("Please use setMetadata") - }) - it("throws on unknown payment providers", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - await expect( - regionService.validateFields_({ payment_providers: ["hi"] }) - ).rejects.toThrow("Provider Not Found") + regionService.validateFields_({ payment_providers: ["should_fail"] }) + ).rejects.toThrow("Payment provider not found") }) it("throws on unknown fulfillment providers", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - await expect( - regionService.validateFields_({ fulfillment_providers: ["hi"] }) - ).rejects.toThrow("Provider Not Found") + regionService.validateFields_({ + fulfillment_providers: ["should_fail"], + }) + ).rejects.toThrow("Fulfillment provider not found") }) }) describe("update", () => { - beforeEach(() => { + 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 = { + 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, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + regionRepository, + countryRepository, + currencyRepository, + storeService, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully updates a region", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - storeService: StoreServiceMock, - }) - - await regionService.update(IdMap.getId("region-se"), { + await regionService.update(IdMap.getId("test-region"), { name: "New Name", - currency_code: "gbp", + currency_code: "eur", tax_rate: 0.25, - countries: ["DK", "se"], + countries: ["US"], payment_providers: ["default_provider"], fulfillment_providers: ["default_provider"], }) - expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledTimes( - 1 - ) - expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledWith( - "default_provider" - ) + expect(ppRepository.findOne).toHaveBeenCalledTimes(1) + expect(fpRepository.findOne).toHaveBeenCalledTimes(1) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledTimes(1) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledWith("default_provider") - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $set: { - name: "New Name", - currency_code: "GBP", - tax_rate: 0.25, - countries: ["DK", "SE"], - payment_providers: ["default_provider"], - fulfillment_providers: ["default_provider"], - }, - } - ) + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-region"), + name: "New Name", + currency_code: "eur", + tax_rate: 0.25, + countries: [{ id: IdMap.getId("test-country"), name: "World" }], + payment_providers: [{ id: "default_provider" }], + fulfillment_providers: [{ id: "default_provider" }], + }) }) }) describe("delete", () => { - beforeAll(() => { + const regionRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("region") }), + }) + + const regionService = new RegionService({ + manager: MockManager, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully deletes", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - }) + await regionService.delete(IdMap.getId("region")) - await regionService.delete(IdMap.getId("region-se")) - - expect(RegionModelMock.deleteOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.deleteOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), + expect(regionRepository.softRemove).toHaveBeenCalledTimes(1) + expect(regionRepository.softRemove).toHaveBeenCalledWith({ + id: IdMap.getId("region"), }) }) }) describe("addCountry", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("region-with-country")) { + return Promise.resolve({ + id: IdMap.getId("region-with-country"), + countries: [ + { id: IdMap.getId("dk"), name: "Denmark", iso_2: "DK" }, + ], + }) + } + return Promise.resolve({ id: IdMap.getId("region") }) + }, + }) + const countryRepository = MockRepository({ + findOne: query => { + if (query.where.iso_2 === "dk") { + return Promise.resolve({ + id: IdMap.getId("dk"), + name: "Denmark", + iso_2: "DK", + }) + } + return Promise.resolve({ + id: IdMap.getId("test-country"), + name: "World", + }) + }, + }) + + const regionService = new RegionService({ + manager: MockManager, + regionRepository, + countryRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully adds to the countries array", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, + await regionService.addCountry(IdMap.getId("region"), "us") + + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(regionRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("region") }, + relations: ["countries"], }) - await regionService.addCountry(IdMap.getId("region-se"), "dk") - - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(2) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - countries: "DK", + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("region"), + countries: [{ id: IdMap.getId("test-country"), name: "World" }], }) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), - }) - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $push: { countries: "DK" }, - } - ) }) it("resolves if exists", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - }) + await regionService.addCountry(IdMap.getId("region-with-country"), "DK") - await regionService.addCountry(IdMap.getId("region-se"), "SE") - - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(2) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - countries: "SE", - }) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), - }) - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(0) + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledTimes(0) }) }) describe("removeCountry", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: query => { + return Promise.resolve({ + id: IdMap.getId("region"), + countries: [{ id: IdMap.getId("dk"), name: "Denmark", iso_2: "dk" }], + }) + }, + }) + + const regionService = new RegionService({ + manager: MockManager, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("successfully removes country", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, + await regionService.removeCountry(IdMap.getId("region"), "dk") + + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("region"), + countries: [], }) - - await regionService.removeCountry(IdMap.getId("region-se"), "dk") - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $pull: { countries: "DK" }, - } - ) }) }) describe("addPaymentProvider", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("region"), + 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, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("successfully adds to the countries array", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - }) - + it("successfully adds payment provider", async () => { await regionService.addPaymentProvider( - IdMap.getId("region-se"), + IdMap.getId("region"), "default_provider" ) - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(ppRepository.findOne).toHaveBeenCalledTimes(1) + + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("region"), + payment_providers: [ + { id: "sweden_provider" }, + { id: "default_provider" }, + ], }) - - expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledTimes( - 1 - ) - expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledWith( - "default_provider" - ) - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $push: { payment_providers: "default_provider" }, - } - ) }) it("resolves if exists", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - paymentProviderService: PaymentProviderServiceMock, - }) - await regionService.addPaymentProvider( - IdMap.getId("region-se"), + IdMap.getId("region"), "sweden_provider" ) - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), - }) - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(0) + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledTimes(0) }) }) describe("addFulfillmentProvider", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("region"), + fulfillment_providers: [{ id: "sweden_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, + fulfillmentProviderRepository: fpRepository, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("successfully adds to the fulfillment_provider array", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - + it("successfully adds payment provider", async () => { await regionService.addFulfillmentProvider( - IdMap.getId("region-se"), + IdMap.getId("region"), "default_provider" ) - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(fpRepository.findOne).toHaveBeenCalledTimes(1) + + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("region"), + fulfillment_providers: [ + { id: "sweden_provider" }, + { id: "default_provider" }, + ], }) - - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledTimes(1) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledWith("default_provider") - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $push: { fulfillment_providers: "default_provider" }, - } - ) }) it("resolves if exists", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - }) - await regionService.addFulfillmentProvider( - IdMap.getId("region-se"), + IdMap.getId("region"), "sweden_provider" ) - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), - }) - - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(0) + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledTimes(0) }) }) describe("removePaymentProvider", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("region"), + payment_providers: [{ id: "sweden_provider" }], + }), + }) + + const regionService = new RegionService({ + manager: MockManager, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("removes payment provider", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - }) - await regionService.removePaymentProvider( - IdMap.getId("region-se"), + IdMap.getId("region"), "sweden_provider" ) - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), - }) + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $pull: { payment_providers: "sweden_provider" }, - } - ) + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("region"), + payment_providers: [], + }) }) }) describe("removeFulfillmentProvider", () => { - beforeEach(() => { + const regionRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("region"), + fulfillment_providers: [{ id: "sweden_provider" }], + }), + }) + + const regionService = new RegionService({ + manager: MockManager, + regionRepository, + }) + + beforeEach(async () => { jest.clearAllMocks() }) - it("removes fulfillment provider", async () => { - const regionService = new RegionService({ - regionModel: RegionModelMock, - }) - + it("removes payment provider", async () => { await regionService.removeFulfillmentProvider( - IdMap.getId("region-se"), + IdMap.getId("region"), "sweden_provider" ) - expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("region-se"), - }) + expect(regionRepository.findOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(RegionModelMock.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("region-se"), - }, - { - $pull: { fulfillment_providers: "sweden_provider" }, - } - ) + expect(regionRepository.save).toHaveBeenCalledTimes(1) + expect(regionRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("region"), + fulfillment_providers: [], + }) }) }) }) diff --git a/packages/medusa/src/services/__tests__/return.js b/packages/medusa/src/services/__tests__/return.js new file mode 100644 index 0000000000..36dbe47c9c --- /dev/null +++ b/packages/medusa/src/services/__tests__/return.js @@ -0,0 +1,274 @@ +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import idMap from "medusa-test-utils/dist/id-map" +import ReturnService from "../return" + +describe("ReturnService", () => { + // describe("requestReturn", () => { + // const returnRepository = MockRepository({}) + + // const fulfillmentProviderService = { + // createReturn: jest.fn().mockImplementation(data => { + // return Promise.resolve(data) + // }), + // } + + // const shippingOptionService = { + // retrieve: jest.fn().mockImplementation(data => { + // return Promise.resolve({ + // id: IdMap.getId("default"), + // name: "default_profile", + // provider_id: "default", + // }) + // }), + // } + + // const totalsService = { + // getTotal: jest.fn().mockImplementation(cart => { + // return 1000 + // }), + // getSubtotal: jest.fn().mockImplementation(cart => { + // return 75 + // }), + // getRefundTotal: jest.fn().mockImplementation((order, lineItems) => { + // return 1000 + // }), + // getRefundedTotal: jest.fn().mockImplementation((order, lineItems) => { + // return 0 + // }), + // } + + // const returnService = new ReturnService({ + // manager: MockManager, + // totalsService, + // shippingOptionService, + // fulfillmentProviderService, + // returnRepository, + // }) + + // beforeEach(async () => { + // jest.clearAllMocks() + // }) + + // it("successfully requests a return", async () => { + // await returnService.requestReturn( + // { + // id: IdMap.getId("test-order"), + // items: [{ id: IdMap.getId("existingLine"), quantity: 10 }], + // tax_rate: 0.25, + // payment_status: "captured", + // }, + // [ + // { + // item_id: IdMap.getId("existingLine"), + // quantity: 10, + // }, + // ], + // { id: "some-shipping-method", price: 150 } + // ) + + // expect(returnRepository.create).toHaveBeenCalledTimes(1) + // expect(returnRepository.create).toHaveBeenCalledWith({ + // status: "requested", + // items: [], + // order_id: IdMap.getId("test-order"), + // shipping_method: { + // id: "some-shipping-method", + // price: 150, + // }, + // shipping_data: { + // id: "some-shipping-method", + // price: 150, + // }, + // refund_amount: 1000 - 150 * (1 + 0.25), + // }) + // }) + + // it("successfully requests a return with custom refund amount", async () => { + // await returnService.requestReturn( + // { + // id: IdMap.getId("test-order"), + // items: [{ id: IdMap.getId("existingLine"), quantity: 10 }], + // tax_rate: 0.25, + // payment_status: "captured", + // }, + // [ + // { + // item_id: IdMap.getId("existingLine"), + // quantity: 10, + // }, + // ], + // { id: "some-shipping-method", price: 150 }, + // 500 + // ) + + // expect(returnRepository.create).toHaveBeenCalledTimes(1) + // expect(returnRepository.create).toHaveBeenCalledWith({ + // status: "requested", + // items: [], + // order_id: IdMap.getId("test-order"), + // shipping_method: { + // id: "some-shipping-method", + // price: 150, + // }, + // shipping_data: expect.anything(), + // refund_amount: 500 - 150 * (1 + 0.25), + // }) + // }) + + // it("throws if refund amount is above captured amount", async () => { + // try { + // await returnService.requestReturn( + // { + // id: IdMap.getId("test-order"), + // items: [{ id: IdMap.getId("existingLine"), quantity: 10 }], + // tax_rate: 0.25, + // payment_status: "captured", + // }, + // [ + // { + // item_id: IdMap.getId("existingLine"), + // quantity: 10, + // }, + // ], + // { id: "some-shipping-method", price: 150 }, + // 2000 + // ) + // } catch (error) { + // expect(error.message).toBe( + // "Cannot refund more than the original payment" + // ) + // } + // }) + // }) + + describe("receiveReturn", () => { + const returnRepository = MockRepository({ + findOne: query => { + if (query.where.id === IdMap.getId("test-return-2")) { + return Promise.resolve({ + id: IdMap.getId("test-return-2"), + status: "requested", + order: { + id: IdMap.getId("test-order"), + items: [ + { id: IdMap.getId("test-line"), quantity: 10 }, + { id: IdMap.getId("test-line-2"), quantity: 10 }, + ], + }, + items: [ + { + item_id: IdMap.getId("test-line"), + quantity: 10, + }, + ], + }) + } + return Promise.resolve({ + id: IdMap.getId("test-return"), + status: "requested", + order: { + id: IdMap.getId("test-order"), + items: [{ id: IdMap.getId("test-line"), quantity: 10 }], + }, + items: [ + { + item_id: IdMap.getId("test-line"), + quantity: 10, + }, + ], + }) + }, + }) + + const totalsService = { + getTotal: jest.fn().mockImplementation(cart => { + return 1000 + }), + getRefundedTotal: jest.fn().mockImplementation((order, lineItems) => { + return 0 + }), + } + + const returnService = new ReturnService({ + manager: MockManager, + totalsService, + returnRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully receives a return", async () => { + await returnService.receiveReturn( + IdMap.getId("test-return"), + [{ item_id: IdMap.getId("test-line"), quantity: 10 }], + 1000 + ) + + expect(returnRepository.save).toHaveBeenCalledTimes(1) + expect(returnRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-return"), + order: { + id: IdMap.getId("test-order"), + items: [{ id: IdMap.getId("test-line"), quantity: 10 }], + }, + status: "received", + items: [ + { + item_id: IdMap.getId("test-line"), + quantity: 10, + is_requested: true, + received_quantity: 10, + requested_quantity: 10, + }, + ], + refund_amount: 1000, + received_at: expect.anything(), + }) + }) + + it("successfully receives a return with requires_action status", async () => { + await returnService.receiveReturn( + IdMap.getId("test-return-2"), + [ + { item_id: IdMap.getId("test-line"), quantity: 10 }, + { item_id: IdMap.getId("test-line-2"), quantity: 10 }, + ], + 1000 + ) + + expect(returnRepository.save).toHaveBeenCalledTimes(1) + expect(returnRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("test-return-2"), + order: { + id: IdMap.getId("test-order"), + items: [ + { id: IdMap.getId("test-line"), quantity: 10 }, + { id: IdMap.getId("test-line-2"), quantity: 10 }, + ], + }, + status: "requires_action", + items: [ + { + item_id: IdMap.getId("test-line"), + quantity: 10, + is_requested: true, + received_quantity: 10, + requested_quantity: 10, + }, + { + return_id: IdMap.getId("test-return-2"), + item_id: IdMap.getId("test-line-2"), + quantity: 10, + is_requested: false, + received_quantity: 10, + metadata: {}, + }, + ], + refund_amount: 1000, + received_at: expect.anything(), + }) + }) + }) +}) diff --git a/packages/medusa/src/services/__tests__/shipping-option.js b/packages/medusa/src/services/__tests__/shipping-option.js index 20ddb4bc8f..15427ad566 100644 --- a/packages/medusa/src/services/__tests__/shipping-option.js +++ b/packages/medusa/src/services/__tests__/shipping-option.js @@ -1,116 +1,63 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import _ from "lodash" +import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import ShippingOptionService from "../shipping-option" -import { ShippingOptionModelMock } from "../../models/__mocks__/shipping-option" -import { RegionServiceMock, regions } from "../__mocks__/region" -import { TotalsServiceMock } from "../__mocks__/totals" -import { - FulfillmentProviderServiceMock, - DefaultProviderMock, -} from "../__mocks__/fulfillment-provider" describe("ShippingOptionService", () => { describe("retrieve", () => { - describe("successfully get profile", () => { - let res - beforeAll(async () => { - const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, - }) - - res = await optionService.retrieve(IdMap.getId("validId")) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", () => { - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("validId"), - }) - }) - - it("returns correct product", () => { - expect(res.name).toEqual("Default Option") - }) - }) - - describe("query fail", () => { - let res - beforeAll(async () => { - const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, - }) - - await optionService.retrieve(IdMap.getId("failId")).catch(err => { - res = err - }) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", () => { - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("failId"), - }) - }) - - it("model query throws error", () => { - expect(res.name).toEqual("not_found") - }) - }) - }) - - describe("setMetadata", () => { - const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, - }) - - beforeEach(() => { + afterAll(() => { jest.clearAllMocks() }) - - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await optionService.setMetadata(`${id}`, "metadata", "testMetadata") - - expect(ShippingOptionModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } - ) + const shippingOptionRepository = MockRepository({ + findOne: () => Promise.resolve({}), + }) + const optionService = new ShippingOptionService({ + manager: MockManager, + shippingOptionRepository, }) - it("throw error on invalid key type", async () => { - try { - optionService.setMetadata(IdMap.getId("test"), 1234, "nono") - } catch (error) { - expect(error.message).toEqual( - "Key type is invalid. Metadata keys must be strings" - ) - } - }) + it("successfully gets shipping option", async () => { + await optionService.retrieve(IdMap.getId("validId")) - it("throws error on invalid optionId type", async () => { - try { - optionService.setMetadata("fakeProfileId", 1234, "nono") - } catch (error) { - expect(error.message).toEqual( - "The shippingOptionId could not be casted to an ObjectId" - ) - } + expect(shippingOptionRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("validId") }, + }) }) }) describe("update", () => { + const shippingOptionRepository = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("noCalc"): + return Promise.resolve({ + provider_id: "no_calc", + }) + case IdMap.getId("validId"): + return Promise.resolve({ + provider_id: "provider", + data: { + provider_data: "true", + }, + }) + + default: + return Promise.resolve({}) + } + }, + }) + + const fulfillmentProviderService = { + canCalculate: jest.fn().mockImplementation(id => id !== "no_calc"), + } + + const shippingOptionRequirementRepository = MockRepository({ + create: r => r, + }) const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, + manager: MockManager, + shippingOptionRepository, + shippingOptionRequirementRepository, + fulfillmentProviderService, }) beforeEach(() => { @@ -118,50 +65,50 @@ describe("ShippingOptionService", () => { }) it("calls updateOne with correct params", async () => { - const id = IdMap.getId("validId") + await optionService.update(IdMap.getId("option"), { name: "new title" }) - await optionService.update(`${id}`, { name: "new title" }) - - expect(ShippingOptionModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { name: "new title" } }, - { runValidators: true } - ) + expect(shippingOptionRepository.save).toBeCalledTimes(1) + expect(shippingOptionRepository.save).toBeCalledWith({ + name: "new title", + }) }) it("sets requirements", async () => { const requirements = [ { type: "min_subtotal", - value: 1, + amount: 1, }, ] - await optionService.update(IdMap.getId("validId"), { requirements }) + await optionService.update(IdMap.getId("option"), { requirements }) - expect(ShippingOptionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("validId") }, - { $set: { requirements } }, - { runValidators: true } - ) + expect(shippingOptionRepository.save).toHaveBeenCalledTimes(1) + expect(shippingOptionRepository.save).toHaveBeenCalledWith({ + requirements: [ + { + shipping_option_id: IdMap.getId("option"), + type: "min_subtotal", + amount: 1, + }, + ], + }) }) it("fails on invalid req", async () => { const requirements = [ { type: "_", - value: 2, + amount: 2, }, { type: "min_subtotal", - value: 1, + amount: 1, }, ] await expect( - optionService.update(IdMap.getId("validId"), { requirements }) + optionService.update(IdMap.getId("option"), { requirements }) ).rejects.toThrow( "Requirement type must be one of min_subtotal, max_subtotal" ) @@ -171,11 +118,11 @@ describe("ShippingOptionService", () => { const requirements = [ { type: "min_subtotal", - value: 2, + amount: 2, }, { type: "min_subtotal", - value: 1, + amount: 1, }, ] @@ -186,56 +133,49 @@ describe("ShippingOptionService", () => { it("sets flat rate price", async () => { await optionService.update(IdMap.getId("validId"), { - price: { - type: "flat_rate", - amount: 100, + price_type: "flat_rate", + amount: 100, + }) + + expect(shippingOptionRepository.save).toHaveBeenCalledTimes(1) + expect(shippingOptionRepository.save).toHaveBeenCalledWith({ + provider_id: "provider", + data: { + provider_data: "true", }, + price_type: "flat_rate", + amount: 100, }) - - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("validId"), - }) - - expect(ShippingOptionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("validId") }, - { $set: { price: { type: "flat_rate", amount: 100 } } }, - { runValidators: true } - ) }) it("sets calculated price", async () => { await optionService.update(IdMap.getId("validId"), { - price: { - type: "calculated", - }, + price_type: "calculated", }) - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("validId"), - }) - - expect(DefaultProviderMock.canCalculate).toHaveBeenCalledTimes(1) - expect(DefaultProviderMock.canCalculate).toHaveBeenCalledWith({ - id: "bonjour", - }) - - expect(ShippingOptionModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("validId") }, - { $set: { price: { type: "calculated" } } }, - { runValidators: true } + expect(fulfillmentProviderService.canCalculate).toHaveBeenCalledTimes(1) + expect(fulfillmentProviderService.canCalculate).toHaveBeenCalledWith( + "provider", + { + provider_data: "true", + } ) + + expect(shippingOptionRepository.save).toHaveBeenCalledTimes(1) + expect(shippingOptionRepository.save).toHaveBeenCalledWith({ + provider_id: "provider", + data: { + provider_data: "true", + }, + price_type: "calculated", + amount: null, + }) }) it("fails on invalid type", async () => { await expect( optionService.update(IdMap.getId("validId"), { - price: { - type: "non", - }, + price_type: "non", }) ).rejects.toThrow("The price must be of type flat_rate or calculated") }) @@ -243,37 +183,13 @@ describe("ShippingOptionService", () => { it("fails if provider cannot calculate", async () => { await expect( optionService.update(IdMap.getId("noCalc"), { - price: { - type: "calculated", - }, + price_type: "calculated", }) ).rejects.toThrow( "The fulfillment provider cannot calculate prices for this option" ) }) - it("throw error on invalid shipping id type", async () => { - await expect( - optionService.update(19314235, { name: "new title" }) - ).rejects.toThrow( - "The shippingOptionId could not be casted to an ObjectId" - ) - }) - - it("throws error when trying to update metadata", async () => { - const id = IdMap.getId("validId") - await expect( - optionService.update(`${id}`, { metadata: { key: "value" } }) - ).rejects.toThrow("Use setMetadata to update metadata fields") - }) - - it("throws error when trying to update region_id", async () => { - const id = IdMap.getId("validId") - await expect( - optionService.update(`${id}`, { region_id: "id" }) - ).rejects.toThrow("Region and Provider cannot be updated after creation") - }) - it("throws error when trying to update region_id", async () => { const id = IdMap.getId("validId") await expect( @@ -290,8 +206,15 @@ describe("ShippingOptionService", () => { }) describe("delete", () => { + const shippingOptionRepository = MockRepository({ + findOne: i => + i.where.id === IdMap.getId("validId") + ? { id: IdMap.getId("validId") } + : null, + }) const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, + manager: MockManager, + shippingOptionRepository, }) beforeEach(() => { @@ -301,22 +224,45 @@ describe("ShippingOptionService", () => { it("deletes the option successfully", async () => { await optionService.delete(IdMap.getId("validId")) - expect(ShippingOptionModelMock.deleteOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.deleteOne).toBeCalledWith({ - _id: IdMap.getId("validId"), + expect(shippingOptionRepository.softRemove).toBeCalledTimes(1) + expect(shippingOptionRepository.softRemove).toBeCalledWith({ + id: IdMap.getId("validId"), }) }) it("is idempotent", async () => { - await optionService.delete(IdMap.getId("delete")) + await expect(optionService.delete(IdMap.getId("delete"))).resolves - expect(ShippingOptionModelMock.deleteOne).toBeCalledTimes(0) + expect(shippingOptionRepository.softRemove).toBeCalledTimes(0) }) }) describe("addRequirement", () => { + const shippingOptionRepository = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("has-min"): + return Promise.resolve({ + requirements: [ + { + type: "min_subtotal", + amount: 1234, + }, + ], + }) + default: + return Promise.resolve({ requirements: [] }) + } + }, + }) + + const shippingOptionRequirementRepository = MockRepository({ + create: r => r, + }) const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, + manager: MockManager, + shippingOptionRepository, + shippingOptionRequirementRepository, }) beforeEach(() => { @@ -326,43 +272,55 @@ describe("ShippingOptionService", () => { it("add product to profile successfully", async () => { await optionService.addRequirement(IdMap.getId("validId"), { type: "max_subtotal", - value: 10, + amount: 10, }) - expect(ShippingOptionModelMock.findOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), + expect(shippingOptionRequirementRepository.create).toBeCalledTimes(1) + expect(shippingOptionRequirementRepository.create).toBeCalledWith({ + type: "max_subtotal", + amount: 10, }) - expect(ShippingOptionModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { - $push: { - requirements: { - type: "max_subtotal", - value: 10, - }, + expect(shippingOptionRepository.save).toBeCalledTimes(1) + expect(shippingOptionRepository.save).toBeCalledWith({ + requirements: [ + { + type: "max_subtotal", + amount: 10, }, - } - ) + ], + }) }) it("fails if type exists", async () => { await expect( - optionService.addRequirement(IdMap.getId("validId"), { + optionService.addRequirement(IdMap.getId("has-min"), { type: "min_subtotal", - value: 100, + amount: 100, }) ).rejects.toThrow("A requirement with type: min_subtotal already exists") - - expect(ShippingOptionModelMock.updateOne).toBeCalledTimes(0) }) }) describe("removeRequirement", () => { + const shippingOptionRepository = MockRepository({ + findOne: q => { + switch (q.where.id) { + default: + return Promise.resolve({ + requirements: [ + { + id: IdMap.getId("requirement_id"), + }, + ], + }) + } + }, + }) + const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, + manager: MockManager, + shippingOptionRepository, }) beforeEach(() => { @@ -372,38 +330,49 @@ describe("ShippingOptionService", () => { it("remove requirement successfully", async () => { await optionService.removeRequirement( IdMap.getId("validId"), - "requirement_id" + IdMap.getId("requirement_id") ) - expect(ShippingOptionModelMock.findOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), - }) - - expect(ShippingOptionModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $pull: { requirements: { _id: "requirement_id" } } } - ) + expect(shippingOptionRepository.save).toBeCalledTimes(1) + expect(shippingOptionRepository.save).toBeCalledWith({ requirements: [] }) }) it("is idempotent", async () => { await optionService.removeRequirement(IdMap.getId("validId"), "something") - expect(ShippingOptionModelMock.findOne).toBeCalledTimes(1) - expect(ShippingOptionModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), + expect(shippingOptionRepository.save).toBeCalledTimes(1) + expect(shippingOptionRepository.save).toBeCalledWith({ + requirements: [{ id: IdMap.getId("requirement_id") }], }) - - expect(ShippingOptionModelMock.updateOne).toBeCalledTimes(0) }) }) describe("create", () => { + const shippingOptionRepository = MockRepository({ + create: r => r, + }) + + const fulfillmentProviderService = { + validateOption: jest.fn().mockImplementation(o => { + return Promise.resolve(o.data.res) + }), + } + + const regionService = { + retrieve: () => { + return Promise.resolve({ fulfillment_providers: [{ id: "provider" }] }) + }, + } + + const shippingOptionRequirementRepository = MockRepository({ + create: r => r, + }) const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, - fulfillmentProviderService: FulfillmentProviderServiceMock, - regionService: RegionServiceMock, + manager: MockManager, + shippingOptionRepository, + shippingOptionRequirementRepository, + fulfillmentProviderService, + regionService, }) beforeEach(() => { @@ -413,44 +382,33 @@ describe("ShippingOptionService", () => { it("creates a shipping option", async () => { const option = { name: "Test Option", - provider_id: "default_provider", + provider_id: "provider", data: { - id: "new", + res: true, }, - region_id: IdMap.getId("region-france"), + region_id: IdMap.getId("reg"), requirements: [ { type: "min_subtotal", - value: 1, + amount: 1, }, ], - price: { - type: "flat_rate", - amount: 13, - }, + price_type: "flat_rate", + amount: 13, } await optionService.create(option) - expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(RegionServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("region-france") + expect(shippingOptionRepository.create).toHaveBeenCalledTimes(1) + expect(shippingOptionRepository.create).toHaveBeenCalledWith(option) + + expect(fulfillmentProviderService.validateOption).toHaveBeenCalledTimes(1) + expect(fulfillmentProviderService.validateOption).toHaveBeenCalledWith( + option ) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledTimes(1) - expect( - FulfillmentProviderServiceMock.retrieveProvider - ).toHaveBeenCalledWith("default_provider") - - expect(DefaultProviderMock.validateOption).toHaveBeenCalledTimes(1) - expect(DefaultProviderMock.validateOption).toHaveBeenCalledWith({ - id: "new", - }) - - expect(ShippingOptionModelMock.create).toHaveBeenCalledTimes(1) - expect(ShippingOptionModelMock.create).toHaveBeenCalledWith(option) + expect(shippingOptionRepository.save).toHaveBeenCalledTimes(1) + expect(shippingOptionRepository.save).toHaveBeenCalledWith(option) }) it("fails if region doesn't have fulfillment provider", async () => { @@ -464,13 +422,11 @@ describe("ShippingOptionService", () => { requirements: [ { type: "min_subtotal", - value: 1, + amount: 1, }, ], - price: { - type: "flat_rate", - amount: 13, - }, + price_type: "flat_rate", + amount: 13, } await expect(optionService.create(option)).rejects.toThrow( @@ -481,15 +437,13 @@ describe("ShippingOptionService", () => { it("fails if fulfillment provider cannot validate", async () => { const option = { name: "Test Option", - provider_id: "default_provider", + provider_id: "provider", data: { - id: "bno", + res: false, }, region_id: IdMap.getId("region-france"), - price: { - type: "flat_rate", - amount: 13, - }, + price_type: "flat_rate", + amount: 13, } await expect(optionService.create(option)).rejects.toThrow( @@ -500,9 +454,9 @@ describe("ShippingOptionService", () => { it("fails if requirement is not validated", async () => { const option = { name: "Test Option", - provider_id: "default_provider", + provider_id: "provider", data: { - id: "new", + res: true, }, requirements: [ { @@ -511,10 +465,8 @@ describe("ShippingOptionService", () => { }, ], region_id: IdMap.getId("region-france"), - price: { - type: "flat_rate", - amount: 13, - }, + price_type: "flat_rate", + amount: 13, } await expect(optionService.create(option)).rejects.toThrow( @@ -525,15 +477,13 @@ describe("ShippingOptionService", () => { it("fails if price is not validated", async () => { const option = { name: "Test Option", - provider_id: "default_provider", + provider_id: "provider", data: { - id: "new", + res: true, }, region_id: IdMap.getId("region-france"), - price: { - type: "nonon", - amount: 13, - }, + price_type: "nonon", + amount: 13, } await expect(optionService.create(option)).rejects.toThrow( @@ -542,20 +492,50 @@ describe("ShippingOptionService", () => { }) }) - describe("setRequirements", () => { - const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, + describe("createShippingMethod", () => { + const option = id => ({ + id, + region_id: IdMap.getId("region"), + price_type: "flat_rate", + amount: 10, + data: { + something: "yes", + }, + requirements: [ + { + type: "min_subtotal", + amount: 100, + }, + ], }) - - beforeEach(() => { - jest.clearAllMocks() + const shippingOptionRepository = MockRepository({ + findOne: q => { + switch (q.where.id) { + default: + return Promise.resolve(option(q.where.id)) + } + }, }) - }) + const shippingMethodRepository = MockRepository({ create: r => r }) + const totalsService = { + getSubtotal: c => { + return c.subtotal + }, + } + + const providerService = { + validateFulfillmentData: jest + .fn() + .mockImplementation(r => Promise.resolve(r.data)), + getPrice: d => d.price, + } - describe("validateCartOption", () => { const optionService = new ShippingOptionService({ - shippingOptionModel: ShippingOptionModelMock, - totalsService: TotalsServiceMock, + manager: MockManager, + shippingMethodRepository, + shippingOptionRepository, + totalsService, + fulfillmentProviderService: providerService, }) beforeEach(() => { @@ -564,54 +544,56 @@ describe("ShippingOptionService", () => { it("validates", async () => { const cart = { - region_id: IdMap.getId("fr-region"), + id: IdMap.getId("cart"), + region_id: IdMap.getId("region"), subtotal: 400, } - const res = await optionService.validateCartOption( - IdMap.getId("validId"), + await optionService.createShippingMethod( + IdMap.getId("option"), + { provider_data: "dat" }, + { cart } + ) + + expect(providerService.validateFulfillmentData).toHaveBeenCalledTimes(1) + expect(providerService.validateFulfillmentData).toHaveBeenCalledWith( + option(IdMap.getId("option")), + { provider_data: "dat" }, cart ) - expect(res).toEqual({ - _id: IdMap.getId("validId"), - name: "Default Option", - region_id: IdMap.getId("fr-region"), - provider_id: "default_provider", - data: { - id: "bonjour", - }, - requirements: [ - { - _id: "requirement_id", - type: "min_subtotal", - value: 100, - }, - ], + expect(shippingMethodRepository.save).toHaveBeenCalledTimes(1) + expect(shippingMethodRepository.save).toHaveBeenCalledWith({ + cart_id: IdMap.getId("cart"), + shipping_option_id: IdMap.getId("option"), price: 10, + data: { something: "yes" }, }) }) it("fails on invalid req", async () => { - const cart = { - region_id: IdMap.getId("nomatch"), - } + const id = IdMap.getId("option") + const d = { some: "thing" } + const c = { region_id: IdMap.getId("nomatch") } await expect( - optionService.validateCartOption(IdMap.getId("validId"), cart) + optionService.createShippingMethod(id, d, { cart: c }) ).rejects.toThrow( "The shipping option is not available in the cart's region" ) }) it("fails if reqs are not satisfied", async () => { + const data = { some: "thing" } const cart = { - region_id: IdMap.getId("fr-region"), + region_id: IdMap.getId("region"), subtotal: 2, } await expect( - optionService.validateCartOption(IdMap.getId("validId"), cart) + optionService.createShippingMethod(IdMap.getId("validId"), data, { + cart, + }) ).rejects.toThrow( "The Cart does not satisfy the shipping option's requirements" ) diff --git a/packages/medusa/src/services/__tests__/shipping-profile.js b/packages/medusa/src/services/__tests__/shipping-profile.js index fc7a986db4..b66adebf64 100644 --- a/packages/medusa/src/services/__tests__/shipping-profile.js +++ b/packages/medusa/src/services/__tests__/shipping-profile.js @@ -1,165 +1,65 @@ -import mongoose from "mongoose" -import { IdMap } from "medusa-test-utils" +import { In } from "typeorm" +import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import ShippingProfileService from "../shipping-profile" -import { ShippingProfileModelMock } from "../../models/__mocks__/shipping-profile" -import { ProductServiceMock, products } from "../__mocks__/product" -import { - ShippingOptionServiceMock, - shippingOptions, -} from "../__mocks__/shipping-option" +//import { ShippingProfileModelMock } from "../../models/__mocks__/shipping-profile" +//import { ProductServiceMock, products } from "../__mocks__/product" +//import { +// ShippingOptionServiceMock, +// shippingOptions, +//} from "../__mocks__/shipping-option" describe("ShippingProfileService", () => { describe("retrieve", () => { describe("successfully get profile", () => { - let res - beforeAll(async () => { - const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - }) - - res = await profileService.retrieve(IdMap.getId("validId")) - }) - afterAll(() => { jest.clearAllMocks() }) - it("calls model layer findOne", () => { - expect(ShippingProfileModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ShippingProfileModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("validId"), + it("calls model layer findOne", async () => { + const profRepo = MockRepository({ + findOne: () => Promise.resolve({}), }) - }) - - it("returns correct product", () => { - expect(res.name).toEqual("Default Profile") - }) - }) - - describe("query fail", () => { - let res - beforeAll(async () => { const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, + manager: MockManager, + shippingProfileRepository: profRepo, }) - await profileService.retrieve(IdMap.getId("failId")).catch(err => { - res = err + await profileService.retrieve(IdMap.getId("validId")) + + expect(profRepo.findOne).toHaveBeenCalledTimes(1) + expect(profRepo.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("validId") }, }) }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls model layer findOne", () => { - expect(ShippingProfileModelMock.findOne).toHaveBeenCalledTimes(1) - expect(ShippingProfileModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("failId"), - }) - }) - - it("model query throws error", () => { - expect(res.name).toEqual("not_found") - }) - }) - }) - - describe("decorate", () => { - const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - productService: ProductServiceMock, - shippingOptionService: ShippingOptionServiceMock, - }) - - const fakeProfile = { - _id: "1234", - name: "Fake", - products: [IdMap.getId("product1")], - shipping_options: [IdMap.getId("franceShipping")], - metadata: {}, - } - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("returns decorated profile", async () => { - const decorated = await profileService.decorate( - fakeProfile, - [], - ["shipping_options"] - ) - expect(decorated).toEqual({ - _id: "1234", - metadata: {}, - shipping_options: [shippingOptions.franceShipping], - }) - }) - - it("returns decorated profile with name", async () => { - const decorated = await profileService.decorate( - fakeProfile, - ["name"], - ["products"] - ) - expect(decorated).toEqual({ - _id: "1234", - metadata: {}, - name: "Fake", - products: [products.product1], - }) - }) - }) - - describe("setMetadata", () => { - const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await profileService.setMetadata(`${id}`, "metadata", "testMetadata") - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } - ) - }) - - it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() - - try { - await profileService.setMetadata(`${id}`, 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "Key type is invalid. Metadata keys must be strings" - ) - } - }) - - it("throws error on invalid profileId type", async () => { - try { - await profileService.setMetadata("fakeProfileId", "1234", "nono") - } catch (err) { - expect(err.message).toEqual( - "The profileId could not be casted to an ObjectId" - ) - } }) }) describe("update", () => { + const profRepo = MockRepository({ + findOne: q => { + return Promise.resolve({ id: q.where.id }) + }, + }) + + const productService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + + const shippingOptionService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - productService: ProductServiceMock, - shippingOptionService: ShippingOptionServiceMock, + manager: MockManager, + shippingProfileRepository: profRepo, + productService, + shippingOptionService, }) beforeEach(() => { @@ -169,119 +69,49 @@ describe("ShippingProfileService", () => { it("calls updateOne with correct params", async () => { const id = IdMap.getId("validId") - await profileService.update(`${id}`, { name: "new title" }) + await profileService.update(id, { name: "new title" }) - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { name: "new title" } }, - { runValidators: true } - ) + expect(profRepo.save).toBeCalledTimes(1) + expect(profRepo.save).toBeCalledWith({ id, name: "new title" }) }) it("calls updateOne products", async () => { const id = IdMap.getId("validId") - await profileService.update(`${id}`, { - products: [IdMap.getId("product1"), IdMap.getId("product1")], + await profileService.update(id, { + products: [IdMap.getId("product1")], }) - expect(ProductServiceMock.retrieve).toBeCalledTimes(1) - expect(ProductServiceMock.retrieve).toBeCalledWith( - IdMap.getId("product1") - ) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { products: [IdMap.getId("product1")] } }, - { runValidators: true } - ) - }) - - it("calls updateOne products", async () => { - const id = IdMap.getId("profile1") - - await profileService.update(`${id}`, { - products: [IdMap.getId("validId")], + expect(productService.update).toBeCalledTimes(1) + expect(productService.update).toBeCalledWith(IdMap.getId("product1"), { + profile_id: id, }) - - expect(ProductServiceMock.retrieve).toBeCalledTimes(1) - expect(ProductServiceMock.retrieve).toBeCalledWith(IdMap.getId("validId")) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(2) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $pull: { products: IdMap.getId("validId") } } - ) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { products: [IdMap.getId("validId")] } }, - { runValidators: true } - ) }) it("calls updateOne with shipping options", async () => { const id = IdMap.getId("profile1") - await profileService.update(`${id}`, { + await profileService.update(id, { shipping_options: [IdMap.getId("validId")], }) - expect(ShippingOptionServiceMock.retrieve).toBeCalledTimes(1) - expect(ShippingOptionServiceMock.retrieve).toBeCalledWith( - IdMap.getId("validId") + expect(shippingOptionService.update).toBeCalledTimes(1) + expect(shippingOptionService.update).toBeCalledWith( + IdMap.getId("validId"), + { profile_id: id } ) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(2) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $pull: { shipping_options: IdMap.getId("validId") } } - ) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { shipping_options: [IdMap.getId("validId")] } }, - { runValidators: true } - ) - }) - - it("calls updateOne with shipping options", async () => { - const id = IdMap.getId("validId") - - await profileService.update(`${id}`, { - shipping_options: [IdMap.getId("validId")], - }) - - expect(ShippingOptionServiceMock.retrieve).toBeCalledTimes(1) - expect(ShippingOptionServiceMock.retrieve).toBeCalledWith( - IdMap.getId("validId") - ) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { shipping_options: [IdMap.getId("validId")] } }, - { runValidators: true } - ) - }) - - it("throw error on invalid product id type", async () => { - await expect( - profileService.update(19314235, { name: "new title" }) - ).rejects.toThrow("The profileId could not be casted to an ObjectId") - }) - - it("throws error when trying to update metadata", async () => { - const id = IdMap.getId("validId") - await expect( - profileService.update(`${id}`, { metadata: { key: "value" } }) - ).rejects.toThrow("Use setMetadata to update metadata fields") }) }) describe("delete", () => { + const profRepo = MockRepository({ + findOne: q => { + return Promise.resolve({ id: q.where.id }) + }, + }) const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, + manager: MockManager, + shippingProfileRepository: profRepo, }) beforeEach(() => { @@ -291,23 +121,27 @@ describe("ShippingProfileService", () => { it("deletes the profile successfully", async () => { await profileService.delete(IdMap.getId("validId")) - expect(ShippingProfileModelMock.deleteOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.deleteOne).toBeCalledWith({ - _id: IdMap.getId("validId"), + expect(profRepo.softRemove).toBeCalledTimes(1) + expect(profRepo.softRemove).toBeCalledWith({ + id: IdMap.getId("validId"), }) }) - - it("is idempotent", async () => { - await profileService.delete(IdMap.getId("delete")) - - expect(ShippingProfileModelMock.deleteOne).toBeCalledTimes(0) - }) }) describe("addProduct", () => { + const profRepo = MockRepository({ findOne: () => Promise.resolve({}) }) + + const productService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - productService: ProductServiceMock, + manager: MockManager, + shippingProfileRepository: profRepo, + productService, }) beforeEach(() => { @@ -320,43 +154,48 @@ describe("ShippingProfileService", () => { IdMap.getId("product2") ) - expect(ProductServiceMock.retrieve).toBeCalledTimes(1) - expect(ProductServiceMock.retrieve).toBeCalledWith( - IdMap.getId("product2") - ) - expect(ShippingProfileModelMock.findOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), + expect(productService.update).toBeCalledTimes(1) + expect(productService.update).toBeCalledWith(IdMap.getId("product2"), { + profile_id: IdMap.getId("validId"), }) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $push: { products: IdMap.getId("product2") } } - ) - }) - - it("is idempotent", async () => { - await profileService.addProduct( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - - expect(ProductServiceMock.retrieve).toBeCalledTimes(1) - expect(ProductServiceMock.retrieve).toBeCalledWith(IdMap.getId("validId")) - expect(ShippingProfileModelMock.findOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), - }) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(0) }) }) describe("fetchCartOptions", () => { + const profRepo = MockRepository({ + find: q => { + switch (q.where.id) { + default: + return Promise.resolve([ + { + shipping_options: [], + }, + ]) + } + }, + }) + + const shippingOptionService = { + list: jest.fn().mockImplementation(() => + Promise.resolve([ + { + id: "ship_1", + }, + { + id: "ship_2", + }, + ]) + ), + validateCartOption: jest.fn().mockImplementation(s => s), + withTransaction: function() { + return this + }, + } + const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - shippingOptionService: ShippingOptionServiceMock, + manager: MockManager, + shippingProfileRepository: profRepo, + shippingOptionService, }) beforeEach(() => { @@ -364,30 +203,58 @@ describe("ShippingProfileService", () => { }) it("fetches correct options", async () => { - await profileService.fetchCartOptions({ + const cart = { items: [ { - content: { product: { _id: IdMap.getId("product_1") } }, + variant: { + product: { + _id: IdMap.getId("product_1"), + profile_id: IdMap.getId("profile"), + }, + }, }, { - content: { product: { _id: IdMap.getId("product_2") } }, + variant: { + product: { + _id: IdMap.getId("product_2"), + profile_id: IdMap.getId("profile"), + }, + }, }, ], - }) + } - expect(ShippingProfileModelMock.find).toBeCalledTimes(1) - expect(ShippingProfileModelMock.find).toBeCalledWith({ - products: { $in: [IdMap.getId("product_1"), IdMap.getId("product_2")] }, - }) + await expect(profileService.fetchCartOptions(cart)).resolves.toEqual([ + { id: "ship_1" }, + { id: "ship_2" }, + ]) - expect(ShippingOptionServiceMock.validateCartOption).toBeCalledTimes(2) + expect(shippingOptionService.validateCartOption).toBeCalledTimes(2) + expect(shippingOptionService.validateCartOption).toBeCalledWith( + { id: "ship_1" }, + cart + ) + expect(shippingOptionService.validateCartOption).toBeCalledWith( + { id: "ship_2" }, + cart + ) }) }) describe("addShippingOption", () => { + const profRepo = MockRepository({ findOne: () => Promise.resolve({}) }) + + const shippingOptionService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - shippingOptionService: ShippingOptionServiceMock, + manager: MockManager, + shippingProfileRepository: profRepo, + shippingOptionService, }) beforeEach(() => { @@ -400,99 +267,19 @@ describe("ShippingProfileService", () => { IdMap.getId("freeShipping") ) - expect(ShippingOptionServiceMock.retrieve).toBeCalledTimes(1) - expect(ShippingOptionServiceMock.retrieve).toBeCalledWith( - IdMap.getId("freeShipping") + expect(shippingOptionService.update).toBeCalledTimes(1) + expect(shippingOptionService.update).toBeCalledWith( + IdMap.getId("freeShipping"), + { profile_id: IdMap.getId("validId") } ) - expect(ShippingProfileModelMock.findOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), - }) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $push: { shipping_options: IdMap.getId("freeShipping") } } - ) - }) - - it("add product is idempotent", async () => { - await profileService.addShippingOption( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - - expect(ShippingOptionServiceMock.retrieve).toBeCalledTimes(1) - expect(ShippingOptionServiceMock.retrieve).toBeCalledWith( - IdMap.getId("validId") - ) - expect(ShippingProfileModelMock.findOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.findOne).toBeCalledWith({ - _id: IdMap.getId("validId"), - }) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(0) - }) - }) - - describe("removeShippingOption", () => { - const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it("deletes a shipping option from a profile", async () => { - await profileService.removeShippingOption( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $pull: { shipping_options: IdMap.getId("validId") } } - ) - }) - }) - - describe("removeProduct", () => { - const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it("deletes a product from a profile", async () => { - await profileService.removeProduct( - IdMap.getId("validId"), - IdMap.getId("validId") - ) - - expect(ShippingProfileModelMock.updateOne).toBeCalledTimes(1) - expect(ShippingProfileModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("validId") }, - { $pull: { products: IdMap.getId("validId") } } - ) - }) - - it("if product does not exist, do nothing", async () => { - await profileService.removeProduct( - IdMap.getId("validId"), - IdMap.getId("produt") - ) - - expect(ShippingProfileModelMock.updateOne).not.toBeCalled() }) }) describe("create", () => { + const profRepo = MockRepository() const profileService = new ShippingProfileService({ - shippingProfileModel: ShippingProfileModelMock, + manager: MockManager, + shippingProfileRepository: profRepo, }) afterEach(() => { @@ -504,8 +291,8 @@ describe("ShippingProfileService", () => { name: "New Profile", }) - expect(ShippingProfileModelMock.create).toHaveBeenCalledTimes(1) - expect(ShippingProfileModelMock.create).toHaveBeenCalledWith({ + expect(profRepo.create).toHaveBeenCalledTimes(1) + expect(profRepo.create).toHaveBeenCalledWith({ name: "New Profile", }) }) diff --git a/packages/medusa/src/services/__tests__/store.js b/packages/medusa/src/services/__tests__/store.js index e3ae44db50..7e9e70b8a6 100644 --- a/packages/medusa/src/services/__tests__/store.js +++ b/packages/medusa/src/services/__tests__/store.js @@ -1,127 +1,176 @@ import StoreService from "../store" -import { StoreModelMock } from "../../models/__mocks__/store" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" describe("StoreService", () => { describe("retrieve", () => { + const storeRepository = MockRepository({}) + const storeService = new StoreService({ - storeModel: StoreModelMock, + manager: MockManager, + storeRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("retrieves store", async () => { + it("successfully retrieve store", async () => { await storeService.retrieve() - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.findOne).toHaveBeenCalledWith() + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) }) describe("update", () => { + const storeRepository = MockRepository({ + findOne: () => + Promise.resolve({ id: IdMap.getId("store"), name: "Medusa" }), + }) + + const currencyRepository = MockRepository({}) + const storeService = new StoreService({ - storeModel: StoreModelMock, + manager: MockManager, + storeRepository, + currencyRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("retrieves store", async () => { + it("successfully updates store", async () => { await storeService.update({ - name: "New Name", - currencies: ["DKK", "sek", "uSd"], + name: "Medusa Commerce", }) - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("store") }, - { - $set: { - name: "New Name", - currencies: ["DKK", "SEK", "USD"], - }, - }, - { runValidators: true } - ) + expect(storeRepository.save).toHaveBeenCalledTimes(1) + expect(storeRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("store"), + name: "Medusa Commerce", + }) }) it("fails if currency not ok", async () => { await expect( storeService.update({ - currencies: ["notacurrence"], + currencies: ["1cd"], }) - ).rejects.toThrow("Invalid currency NOTACURRENCE") + ).rejects.toThrow("Invalid currency 1cd") - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) }) describe("addCurrency", () => { + const storeRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("store"), + name: "Medusa", + currencies: [{ code: "dkk" }], + }), + }) + + const currencyRepository = MockRepository({ + findOne: query => { + if (query.where.code === "sek") { + return Promise.resolve({ code: "sek" }) + } + + if (query.where.code === "dkk") { + return Promise.resolve({ code: "dkk" }) + } + return Promise.resolve() + }, + }) + const storeService = new StoreService({ - storeModel: StoreModelMock, + manager: MockManager, + storeRepository, + currencyRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("retrieves store", async () => { + it("successfully adds currency", async () => { await storeService.addCurrency("sek") - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("store") }, - { - $push: { currencies: "SEK" }, - } - ) + expect(storeRepository.save).toHaveBeenCalledTimes(1) + expect(storeRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("store"), + name: "Medusa", + currencies: [{ code: "dkk" }, { code: "sek" }], + }) }) it("fails if currency not ok", async () => { - await expect(storeService.addCurrency("notacurrence")).rejects.toThrow( - "Invalid currency NOTACURRENCE" + await expect(storeService.addCurrency("1cd")).rejects.toThrow( + "Currency 1cd not found" ) - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) it("fails if currency already existis", async () => { - await expect(storeService.addCurrency("DKK")).rejects.toThrow( + await expect(storeService.addCurrency("dkk")).rejects.toThrow( "Currency already added" ) - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) }) describe("removeCurrency", () => { + const storeRepository = MockRepository({ + findOne: () => + Promise.resolve({ + id: IdMap.getId("store"), + name: "Medusa", + currencies: [{ code: "dkk" }], + }), + }) + + const currencyRepository = MockRepository({ + findOne: query => { + if (query.where.code === "sek") { + return Promise.resolve({ code: "sek" }) + } + + if (query.where.code === "dkk") { + return Promise.resolve({ code: "dkk" }) + } + return Promise.resolve() + }, + }) + const storeService = new StoreService({ - storeModel: StoreModelMock, + manager: MockManager, + storeRepository, + currencyRepository, }) beforeEach(() => { jest.clearAllMocks() }) - it("retrieves store", async () => { - await storeService.removeCurrency("sek") + it("successfully removes currency", async () => { + await storeService.removeCurrency("dkk") - expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.findOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1) - expect(StoreModelMock.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("store") }, - { - $pull: { currencies: "SEK" }, - } - ) + expect(storeRepository.save).toHaveBeenCalledTimes(1) + expect(storeRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("store"), + currencies: [], + name: "Medusa", + }) }) }) }) diff --git a/packages/medusa/src/services/__tests__/swap.js b/packages/medusa/src/services/__tests__/swap.js index 5fe21ee4e4..eb5a7dd31a 100644 --- a/packages/medusa/src/services/__tests__/swap.js +++ b/packages/medusa/src/services/__tests__/swap.js @@ -1,14 +1,9 @@ -import { IdMap } from "medusa-test-utils" -import { ProductVariantServiceMock } from "../__mocks__/product-variant" -import { - // FulfillmentProviderServiceMock, - DefaultProviderMock as FulfillmentProviderMock, -} from "../__mocks__/fulfillment-provider" +import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import SwapService from "../swap" const generateOrder = (orderId, items, additional = {}) => { return { - _id: IdMap.getId(orderId), + id: IdMap.getId(orderId), items: items.map( ({ id, @@ -19,16 +14,15 @@ const generateOrder = (orderId, items, additional = {}) => { quantity, price, }) => ({ - _id: IdMap.getId(id), - content: { + id: IdMap.getId(id), + variant_id: IdMap.getId(variant_id), + variant: { + id: IdMap.getId(variant_id), product: { - _id: IdMap.getId(product_id), + id: IdMap.getId(product_id), }, - variant: { - _id: IdMap.getId(variant_id), - }, - unit_price: price, }, + unit_price: price, quantity, fulfilled_quantity: fulfilled || 0, returned_quantity: returned || 0, @@ -53,7 +47,7 @@ const testOrder = generateOrder( { fulfillment_status: "fulfilled", payment_status: "captured", - currency_code: "DKK", + currency_code: "dkk", region_id: IdMap.getId("region"), tax_rate: 0, shipping_address: { @@ -69,29 +63,6 @@ const testOrder = generateOrder( } ) -const SwapModel = ({ create, updateOne, findOne } = {}) => { - return { - create: jest.fn().mockImplementation((...args) => { - if (create) { - return create(...args) - } - return Promise.resolve({ data: "swap" }) - }), - updateOne: jest.fn().mockImplementation((...args) => { - if (updateOne) { - return updateOne(...args) - } - return Promise.resolve({ data: "swap" }) - }), - findOne: jest.fn().mockImplementation((...args) => { - if (findOne) { - return findOne(...args) - } - return Promise.resolve({ data: "swap" }) - }), - } -} - describe("SwapService", () => { describe("validateReturnItems_", () => { beforeEach(() => { @@ -105,7 +76,7 @@ describe("SwapService", () => { { items: [ { - _id: IdMap.getId("line1"), + id: IdMap.getId("line1"), quantity: 1, returned_quantity: 1, }, @@ -124,7 +95,7 @@ describe("SwapService", () => { { items: [ { - _id: IdMap.getId("line1"), + id: IdMap.getId("line1"), quantity: 1, returned_quantity: 1, }, @@ -142,7 +113,7 @@ describe("SwapService", () => { { items: [ { - _id: IdMap.getId("line1"), + id: IdMap.getId("line1"), quantity: 1, returned_quantity: 0, }, @@ -162,85 +133,91 @@ describe("SwapService", () => { describe("success", () => { const existing = { - _id: IdMap.getId("test-swap"), + id: IdMap.getId("test-swap"), order_id: IdMap.getId("test"), - return: { - _id: IdMap.getId("return-swap"), + order: testOrder, + return_order: { + id: IdMap.getId("return-swap"), test: "notreceived", refund_amount: 11, + items: [{ item_id: IdMap.getId("line"), quantity: 1 }], }, - return_items: [{ item_id: IdMap.getId("line"), quantity: 1 }], - additional_items: [{ data: "lines" }], + additional_items: [{ data: "lines", id: "test" }], other: "data", } const cartService = { - create: jest - .fn() - .mockReturnValue(Promise.resolve({ _id: IdMap.getId("swap-cart") })), + create: jest.fn().mockReturnValue(Promise.resolve({ id: "cart" })), + update: jest.fn().mockReturnValue(Promise.resolve()), + withTransaction: function() { + return this + }, } - const swapModel = SwapModel({ findOne: () => Promise.resolve(existing) }) + + const swapRepo = MockRepository({ + findOne: () => Promise.resolve(existing), + }) + + const lineItemService = { + create: jest.fn().mockImplementation(d => Promise.resolve(d)), + update: jest.fn().mockImplementation(d => Promise.resolve(d)), + withTransaction: function() { + return this + }, + } + const swapService = new SwapService({ - productVariantService: ProductVariantServiceMock, - swapModel, + manager: MockManager, + swapRepository: swapRepo, cartService, + lineItemService, }) it("finds swap and calls return create cart", async () => { - await swapService.createCart(testOrder, IdMap.getId("swap-1")) + await swapService.createCart(IdMap.getId("swap-1")) - expect(swapModel.findOne).toHaveBeenCalledTimes(1) - expect(swapModel.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("swap-1"), + expect(swapRepo.findOne).toHaveBeenCalledTimes(1) + expect(swapRepo.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("swap-1") }, + relations: [ + "order", + "order.items", + "order.discounts", + "additional_items", + "return_order", + "return_order.items", + "return_order.shipping_method", + ], }) expect(cartService.create).toHaveBeenCalledTimes(1) expect(cartService.create).toHaveBeenCalledWith({ email: testOrder.email, - shipping_address: testOrder.shipping_address, - billing_address: testOrder.billing_address, - items: [ - { - _id: IdMap.getId("line"), - content: { - variant: { - _id: IdMap.getId("variant"), - }, - product: { - _id: IdMap.getId("product"), - }, - unit_price: -100, - }, - quantity: 1, - fulfilled_quantity: 1, - returned_quantity: 0, - metadata: { - is_return_line: true, - }, - }, - ...existing.additional_items, - ], + discounts: testOrder.discounts, region_id: testOrder.region_id, customer_id: testOrder.customer_id, - is_swap: true, + type: "swap", metadata: { swap_id: IdMap.getId("test-swap"), parent_order_id: IdMap.getId("test"), }, }) - expect(swapModel.updateOne).toHaveBeenCalledTimes(1) - expect(swapModel.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("swap-1") }, - { $set: { cart_id: IdMap.getId("swap-cart") } } - ) + expect(cartService.create).toHaveBeenCalledTimes(1) + // expect(cartService.update).toHaveBeenCalledTimes(1) + + expect(swapRepo.save).toHaveBeenCalledTimes(1) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing, + cart_id: "cart", + }) }) }) describe("failure", () => { const existing = { - return: { - _id: IdMap.getId("return-swap"), + return_order: { + id: IdMap.getId("return-swap"), test: "notreceived", refund_amount: 11, }, @@ -248,20 +225,8 @@ describe("SwapService", () => { other: "data", } - it("fails if swap doesn't belong to order", async () => { - const swapModel = SwapModel({ - findOne: () => Promise.resolve(existing), - }) - const swapService = new SwapService({ swapModel }) - const res = swapService.createCart(testOrder, IdMap.getId("swap-1")) - - await expect(res).rejects.toThrow( - "The swap does not belong to the order" - ) - }) - it("fails if cart already created", async () => { - const swapModel = SwapModel({ + const swapRepo = MockRepository({ findOne: () => Promise.resolve({ ...existing, @@ -269,8 +234,11 @@ describe("SwapService", () => { cart_id: IdMap.getId("swap-cart"), }), }) - const swapService = new SwapService({ swapModel }) - const res = swapService.createCart(testOrder, IdMap.getId("swap-1")) + const swapService = new SwapService({ + manager: MockManager, + swapRepository: swapRepo, + }) + const res = swapService.createCart(IdMap.getId("swap-1")) await expect(res).rejects.toThrow( "A cart has already been created for the swap" @@ -288,26 +256,26 @@ describe("SwapService", () => { const lineItemService = { generate: jest .fn() - .mockImplementation((variantId, regionId, quantity, metadata) => { + .mockImplementation((variantId, regionId, quantity) => { return { - content: { - unit_price: 100, - variant: { - _id: variantId, - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, + unit_price: 100, + variant_id: variantId, quantity, } }), } - const swapModel = SwapModel() + const swapRepo = MockRepository() + const returnService = { + create: jest.fn().mockReturnValue(Promise.resolve({ id: "ret" })), + withTransaction: function() { + return this + }, + } + const swapService = new SwapService({ - swapModel, - productVariantService: ProductVariantServiceMock, + manager: MockManager, + swapRepository: swapRepo, + returnService, lineItemService, }) @@ -341,31 +309,20 @@ describe("SwapService", () => { } ) - expect(swapModel.create).toHaveBeenCalledWith({ + expect(swapRepo.create).toHaveBeenCalledWith({ order_id: IdMap.getId("test"), - return_items: [{ item_id: IdMap.getId("line"), quantity: 1 }], - region_id: IdMap.getId("region"), - currency_code: "DKK", - return_shipping: { - id: IdMap.getId("return-shipping"), - price: 20, - }, + fulfillment_status: "not_fulfilled", + payment_status: "not_paid", additional_items: [ { - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("new-variant"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, + unit_price: 100, + variant_id: IdMap.getId("new-variant"), quantity: 1, }, ], }) + + expect(returnService.create).toHaveBeenCalledTimes(1) }) }) }) @@ -380,42 +337,41 @@ describe("SwapService", () => { receiveReturn: jest .fn() .mockReturnValue(Promise.resolve({ test: "received" })), + withTransaction: function() { + return this + }, } const existing = { order_id: IdMap.getId("test"), - return: { - _id: IdMap.getId("return-swap"), + return_id: IdMap.getId("test"), + return_order: { + id: IdMap.getId("return-swap"), test: "notreceived", refund_amount: 11, }, other: "data", } - const swapModel = SwapModel({ findOne: () => Promise.resolve(existing) }) - const swapService = new SwapService({ swapModel, returnService }) + const swapRepo = MockRepository({ + findOne: () => Promise.resolve(existing), + }) + const swapService = new SwapService({ + manager: MockManager, + swapRepository: swapRepo, + returnService, + }) it("calls register return and updates return value", async () => { - await swapService.receiveReturn(testOrder, IdMap.getId("swap"), [ - { variant_id: IdMap.getId("1234"), quantity: 1 }, + await swapService.receiveReturn(IdMap.getId("swap"), [ + { item_id: IdMap.getId("1234"), quantity: 1 }, ]) - expect(swapModel.updateOne).toHaveBeenCalledWith( - { _id: IdMap.getId("swap") }, - { - $set: { - status: "received", - return: { test: "received" }, - }, - } - ) - expect(returnService.receiveReturn).toHaveBeenCalledTimes(1) expect(returnService.receiveReturn).toHaveBeenCalledWith( - testOrder, - existing.return, - [{ variant_id: IdMap.getId("1234"), quantity: 1 }], - 11, + IdMap.getId("return-swap"), + [{ item_id: IdMap.getId("1234"), quantity: 1 }], + undefined, false ) }) @@ -426,56 +382,49 @@ describe("SwapService", () => { receiveReturn: jest .fn() .mockReturnValue(Promise.resolve({ status: "requires_action" })), + withTransaction: function() { + return this + }, } const existing = { order_id: IdMap.getId("test"), - return: { - _id: IdMap.getId("return-swap"), - test: "notreceived", - refund_amount: 11, - }, + return_id: IdMap.getId("return-swap"), + return_order_id: IdMap.getId("return-swap"), + return_order: { id: IdMap.getId("return-swap") }, other: "data", } - const swapModel = SwapModel({ - findOne: t => - Promise.resolve(t._id.equals(IdMap.getId("empty")) ? {} : existing), + const swapRepo = MockRepository({ + findOne: q => + Promise.resolve(q.where.id === IdMap.getId("empty") ? {} : existing), + }) + const swapService = new SwapService({ + manager: MockManager, + swapRepository: swapRepo, + returnService, }) - const swapService = new SwapService({ swapModel, returnService }) it("fails if swap has no return request", async () => { - const res = swapService.receiveReturn( - testOrder, - IdMap.getId("empty"), - [] - ) + const res = swapService.receiveReturn(IdMap.getId("empty"), []) await expect(res).rejects.toThrow("Swap has no return request") }) it("sets requires action if return fails", async () => { - await swapService.receiveReturn(testOrder, IdMap.getId("swap"), [ + await swapService.receiveReturn(IdMap.getId("swap"), [ { variant_id: IdMap.getId("1234"), quantity: 1 }, ]) - expect(swapModel.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("swap"), - }, - { - $set: { - status: "requires_action", - return: { status: "requires_action" }, - }, - } - ) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing, + fulfillment_status: "requires_action", + }) expect(returnService.receiveReturn).toHaveBeenCalledTimes(1) expect(returnService.receiveReturn).toHaveBeenCalledWith( - testOrder, - existing.return, + IdMap.getId("return-swap"), [{ variant_id: IdMap.getId("1234"), quantity: 1 }], - 11, + undefined, false ) }) @@ -491,46 +440,67 @@ describe("SwapService", () => { const fulfillmentService = { createFulfillment: jest .fn() - .mockReturnValue(Promise.resolve([{ data: "new" }])), + .mockReturnValue( + Promise.resolve([ + { items: [{ item_id: "1234", quantity: 2 }], data: "new" }, + ]) + ), + withTransaction: function() { + return this + }, } const existing = { + fulfillment_status: "not_fulfilled", + order: testOrder, additional_items: [ { - _id: IdMap.getId("1234"), + id: "1234", quantity: 2, }, ], shipping_methods: [{ method: "1" }], - return: { - _id: IdMap.getId("return-swap"), - test: "notreceived", - refund_amount: 11, - }, other: "data", } - const swapModel = SwapModel({ findOne: () => existing }) - const swapService = new SwapService({ swapModel, fulfillmentService }) + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + + const swapRepo = MockRepository({ + findOne: () => Promise.resolve({ ...existing }), + }) + const swapService = new SwapService({ + manager: MockManager, + swapRepository: swapRepo, + fulfillmentService, + lineItemService, + }) it("creates a fulfillment", async () => { - await swapService.createFulfillment(testOrder, IdMap.getId("swap")) + await swapService.createFulfillment(IdMap.getId("swap")) - expect(swapModel.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("swap"), - }, - { - $set: { - fulfillment_status: "fulfilled", - fulfillments: [{ data: "new" }], - }, - } - ) + expect(lineItemService.update).toHaveBeenCalledTimes(1) + expect(lineItemService.update).toHaveBeenCalledWith("1234", { + fulfilled_quantity: 2, + }) + + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing, + fulfillment_status: "fulfilled", + fulfillments: [ + { items: [{ item_id: "1234", quantity: 2 }], data: "new" }, + ], + }) expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith( { ...existing, + email: testOrder.email, + discounts: testOrder.discounts, currency_code: testOrder.currency_code, tax_rate: testOrder.tax_rate, region_id: testOrder.region_id, @@ -540,8 +510,8 @@ describe("SwapService", () => { items: existing.additional_items, shipping_methods: existing.shipping_methods, }, - [{ item_id: IdMap.getId("1234"), quantity: 2 }], - {} + [{ item_id: "1234", quantity: 2 }], + { swap_id: IdMap.getId("swap"), metadata: {} } ) }) }) @@ -557,38 +527,51 @@ describe("SwapService", () => { describe("success", () => { const fulfillmentService = { createShipment: jest.fn().mockImplementation((o, f) => { - return Promise.resolve({ ...f, data: "new" }) + return Promise.resolve({ + items: [ + { + item_id: IdMap.getId("1234-1"), + quantity: 2, + }, + ], + data: "new", + }) }), + withTransaction: function() { + return this + }, } const eventBusService = { emit: jest.fn().mockReturnValue(Promise.resolve()), + withTransaction: function() { + return this + }, } const existing = { + fulfillment_status: "not_fulfilled", additional_items: [ { - _id: IdMap.getId("1234-1"), + id: IdMap.getId("1234-1"), quantity: 2, shipped_quantity: 0, }, ], fulfillments: [ { - _id: IdMap.getId("f1"), + id: IdMap.getId("f1"), items: [ { - _id: IdMap.getId("1234-1"), item_id: IdMap.getId("1234-1"), quantity: 2, }, ], }, { - _id: IdMap.getId("f2"), + id: IdMap.getId("f2"), items: [ { - _id: IdMap.getId("1234-2"), item_id: IdMap.getId("1234-2"), quantity: 2, }, @@ -596,19 +579,24 @@ describe("SwapService", () => { }, ], shipping_methods: [{ method: "1" }], - return: { - _id: IdMap.getId("return-swap"), - test: "notreceived", - refund_amount: 11, - }, other: "data", } - const swapModel = SwapModel({ - updateOne: () => Promise.resolve(existing), - findOne: () => existing, + + const lineItemService = { + update: jest.fn(), + withTransaction: function() { + return this + }, + } + + const swapRepo = MockRepository({ + findOne: () => Promise.resolve(existing), }) + const swapService = new SwapService({ - swapModel, + manager: MockManager, + swapRepository: swapRepo, + lineItemService, eventBusService, fulfillmentService, }) @@ -621,63 +609,20 @@ describe("SwapService", () => { {} ) - expect(swapModel.updateOne).toHaveBeenCalledWith( + expect(lineItemService.update).toHaveBeenCalledWith( + IdMap.getId("1234-1"), { - _id: IdMap.getId("swap"), - }, - { - $set: { - additional_items: [ - { - _id: IdMap.getId("1234-1"), - quantity: 2, - shipped: true, - shipped_quantity: 2, - }, - ], - fulfillment_status: "shipped", - fulfillments: [ - { - _id: IdMap.getId("f1"), - items: [ - { - _id: IdMap.getId("1234-1"), - item_id: IdMap.getId("1234-1"), - quantity: 2, - }, - ], - data: "new", - }, - { - _id: IdMap.getId("f2"), - items: [ - { - _id: IdMap.getId("1234-2"), - item_id: IdMap.getId("1234-2"), - quantity: 2, - }, - ], - }, - ], - }, + shipped_quantity: 2, } ) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing, + fulfillment_status: "shipped", + }) + expect(fulfillmentService.createShipment).toHaveBeenCalledWith( - { - items: existing.additional_items, - shipping_methods: existing.shipping_methods, - }, - { - _id: IdMap.getId("f1"), - items: [ - { - _id: IdMap.getId("1234-1"), - item_id: IdMap.getId("1234-1"), - quantity: 2, - }, - ], - }, + IdMap.getId("f1"), ["1234"], {} ) @@ -687,43 +632,217 @@ describe("SwapService", () => { describe("failure", () => {}) }) - describe("requestReturn", () => { + describe("registerCartCompletion", () => { + beforeEach(() => { + jest.clearAllMocks() + Date.now = jest.fn(() => 1572393600000) + }) + + describe("success", () => { + const eventBusService = { + emit: jest.fn().mockReturnValue(Promise.resolve()), + withTransaction: function() { + return this + }, + } + + const totalsService = { + getTotal: () => { + return Promise.resolve(100) + }, + } + + const shippingOptionService = { + updateShippingMethod: () => { + return Promise.resolve() + }, + withTransaction: function() { + return this + }, + } + + const paymentProviderService = { + getStatus: jest.fn(() => { + return Promise.resolve("authorized") + }), + updatePayment: jest.fn(() => { + return Promise.resolve() + }), + withTransaction: function() { + return this + }, + } + + const existing = { + cart: { + items: [{ id: "1" }], + shipping_methods: [{ id: "method_1" }], + payment: { + good: "yes", + }, + shipping_address_id: 1234, + }, + other: "data", + } + + const swapRepo = MockRepository({ + findOne: () => Promise.resolve(existing), + }) + + const swapService = new SwapService({ + manager: MockManager, + swapRepository: swapRepo, + totalsService, + paymentProviderService, + eventBusService, + shippingOptionService, + }) + + it("creates a shipment", async () => { + await swapService.registerCartCompletion(IdMap.getId("swap")) + + expect(paymentProviderService.getStatus).toHaveBeenCalledWith({ + good: "yes", + }) + + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing, + difference_due: 100, + shipping_address_id: 1234, + confirmed_at: expect.anything(), + }) + }) + }) + }) + + describe("processDifference", () => { beforeEach(() => { jest.clearAllMocks() }) describe("success", () => { - const existing = { - return_items: [{ data: "returnline" }], - return_shipping: { shipping: "return" }, + const eventBusService = { + emit: jest.fn().mockReturnValue(Promise.resolve()), + withTransaction: function() { + return this + }, } - const returnService = { - requestReturn: jest - .fn() - .mockReturnValue(Promise.resolve({ return: "data" })), + const paymentProviderService = { + capturePayment: jest.fn(g => + g.id === "good" ? Promise.resolve() : Promise.reject() + ), + refundPayment: jest.fn(g => + g[0].id === "good" ? Promise.resolve() : Promise.reject() + ), + withTransaction: function() { + return this + }, } - const swapModel = SwapModel({ findOne: () => existing }) - const swapService = new SwapService({ - swapModel, - returnService, + + const existing = (dif, fail, conf = true) => ({ + confirmed_at: conf ? "1234" : null, + difference_due: dif, + payment: { id: fail ? "f" : "good" }, + order: { + payments: [{ id: fail ? "f" : "good" }], + }, }) - it("calls requestReturn and updates", async () => { - await swapService.requestReturn(testOrder, IdMap.getId("swap")) + const swapRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case "refund": + return Promise.resolve(existing(-1, false)) + case "refund_fail": + return Promise.resolve(existing(-1, true)) + case "capture_fail": + return Promise.resolve(existing(1, true)) + case "0": + return Promise.resolve(existing(0, false)) + case "not_conf": + return Promise.resolve(existing(1, false, false)) + default: + return Promise.resolve(existing(1, false)) + } + }, + }) - expect(returnService.requestReturn).toHaveBeenCalledTimes(1) - expect(returnService.requestReturn).toHaveBeenCalledWith( - testOrder, - existing.return_items, - existing.return_shipping + const swapService = new SwapService({ + manager: MockManager, + swapRepository: swapRepo, + paymentProviderService, + eventBusService, + }) + + it("capture success", async () => { + await swapService.processDifference(IdMap.getId("swap")) + expect(paymentProviderService.capturePayment).toHaveBeenCalledWith({ + id: "good", + }) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing(1, false), + payment_status: "captured", + }) + }) + + it("capture fail", async () => { + await swapService.processDifference("capture_fail") + expect(paymentProviderService.capturePayment).toHaveBeenCalledWith({ + id: "f", + }) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing(1, true), + payment_status: "requires_action", + }) + }) + + it("refund success", async () => { + await swapService.processDifference("refund") + expect(paymentProviderService.refundPayment).toHaveBeenCalledWith( + [ + { + id: "good", + }, + ], + 1, + "swap" ) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing(-1, false), + payment_status: "difference_refunded", + }) + }) - expect(swapModel.updateOne).toHaveBeenCalledWith( - { - _id: IdMap.getId("swap"), - }, - { $set: { return: { return: "data" } } } + it("refund fail", async () => { + await swapService.processDifference("refund_fail") + + expect(paymentProviderService.refundPayment).toHaveBeenCalledWith( + [ + { + id: "f", + }, + ], + 1, + "swap" + ) + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing(-1, true), + payment_status: "requires_action", + }) + }) + + it("zero", async () => { + await swapService.processDifference("0") + expect(swapRepo.save).toHaveBeenCalledWith({ + ...existing(0, false), + payment_status: "difference_refunded", + }) + }) + + it("not confirmed", async () => { + await expect(swapService.processDifference("not_conf")).rejects.toThrow( + "Cannot process a swap that hasn't been confirmed by the customer" ) }) }) diff --git a/packages/medusa/src/services/__tests__/totals.js b/packages/medusa/src/services/__tests__/totals.js index 217e559e30..9b5cb1060c 100644 --- a/packages/medusa/src/services/__tests__/totals.js +++ b/packages/medusa/src/services/__tests__/totals.js @@ -1,89 +1,185 @@ import TotalsService from "../totals" -import { ProductVariantServiceMock } from "../__mocks__/product-variant" -import { discounts } from "../../models/__mocks__/discount" -import { carts } from "../__mocks__/cart" -import { orders } from "../../models/__mocks__/order" import { IdMap } from "medusa-test-utils" -import { RegionServiceMock } from "../__mocks__/region" + +const discounts = { + total10Percent: { + id: "total10", + code: "10%OFF", + rule: { + type: "percentage", + allocation: "total", + value: 10, + }, + regions: [{ id: "fr" }], + }, + item2Fixed: { + id: "item2Fixed", + code: "MEDUSA", + rule: { + type: "fixed", + allocation: "item", + value: 2, + valid_for: [{ id: "testp2" }], + }, + regions: [{ id: "fr" }], + }, + item10Percent: { + id: "item10Percent", + code: "MEDUSA", + rule: { + type: "percentage", + allocation: "item", + value: 10, + valid_for: [{ id: "testp2" }], + }, + regions: [{ id: "fr" }], + }, + total10Fixed: { + id: "total10Fixed", + code: "MEDUSA", + rule: { + type: "fixed", + allocation: "total", + value: 10, + valid_for: [], + }, + regions: [{ id: "fr" }], + }, + expiredDiscount: { + id: "expired", + code: "MEDUSA", + ends_at: new Date("December 17, 1995 03:24:00"), + rule: { + type: "fixed", + allocation: "item", + value: 10, + valid_for: [], + }, + regions: [{ id: "fr" }], + }, +} describe("TotalsService", () => { describe("getAllocationItemDiscounts", () => { let res - const totalsService = new TotalsService({ - productVariantService: ProductVariantServiceMock, - }) + const totalsService = new TotalsService() beforeEach(() => { jest.clearAllMocks() }) it("calculates item with percentage discount", async () => { - res = await totalsService.getAllocationItemDiscounts( - discounts.item10Percent, - carts.frCart - ) + const cart = { + items: [ + { + id: "test", + allow_discounts: true, + unit_price: 10, + quantity: 10, + variant: { + id: "testv", + product_id: "testp", + }, + }, + ], + } + + const discount = { + rule: { + type: "percentage", + value: 10, + valid_for: [{ id: "testp" }], + }, + } + + res = totalsService.getAllocationItemDiscounts(discount, cart) expect(res).toEqual([ { lineItem: { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, + id: "test", + allow_discounts: true, + unit_price: 10, quantity: 10, + variant: { + id: "testv", + product_id: "testp", + }, }, - variant: IdMap.getId("eur-10-us-12"), + variant: "testv", amount: 10, }, ]) }) it("calculates item with fixed discount", async () => { - res = await totalsService.getAllocationItemDiscounts( - discounts.item9Fixed, - carts.frCart - ) + const cart = { + items: [ + { + id: "exists", + allow_discounts: true, + unit_price: 10, + variant: { + id: "testv", + product_id: "testp", + }, + quantity: 10, + }, + ], + } + + const discount = { + rule: { + type: "fixed", + value: 9, + valid_for: [{ id: "testp" }], + }, + } + + res = totalsService.getAllocationItemDiscounts(discount, cart) expect(res).toEqual([ { lineItem: { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 10, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "exists", + allow_discounts: true, + unit_price: 10, + variant: { + id: "testv", + product_id: "testp", }, quantity: 10, }, - variant: IdMap.getId("eur-10-us-12"), + variant: "testv", amount: 90, }, ]) }) it("does not apply discount if no valid variants are provided", async () => { - res = await totalsService.getAllocationItemDiscounts( - discounts.item10FixedNoVariants, - carts.frCart - ) + const cart = { + items: [ + { + id: "exists", + allow_discounts: true, + unit_price: 10, + variant: { + id: "testv", + product_id: "testp", + }, + quantity: 10, + }, + ], + } + + const discount = { + rule: { + type: "fixed", + value: 9, + valid_for: [], + }, + } + res = totalsService.getAllocationItemDiscounts(discount, cart) expect(res).toEqual([]) }) @@ -91,58 +187,87 @@ describe("TotalsService", () => { describe("getDiscountTotal", () => { let res - const totalsService = new TotalsService({ - productVariantService: ProductVariantServiceMock, - }) + const totalsService = new TotalsService() + + const discountCart = { + id: "discount_cart", + discounts: [], + region_id: "fr", + items: [ + { + id: "line", + allow_discounts: true, + unit_price: 18, + variant: { + id: "testv1", + product_id: "testp1", + }, + quantity: 10, + }, + { + id: "line2", + allow_discounts: true, + unit_price: 10, + variant: { + id: "testv2", + product_id: "testp2", + }, + quantity: 10, + }, + ], + } beforeEach(() => { jest.clearAllMocks() - carts.discountCart.discounts = [] + discountCart.discounts = [] }) it("calculate total precentage discount", async () => { - carts.discountCart.discounts.push(discounts.total10Percent) - res = await totalsService.getDiscountTotal(carts.discountCart) + discountCart.discounts.push(discounts.total10Percent) + res = totalsService.getDiscountTotal(discountCart) expect(res).toEqual(28) }) it("calculate item fixed discount", async () => { - carts.discountCart.discounts.push(discounts.item2Fixed) - res = await totalsService.getDiscountTotal(carts.discountCart) + discountCart.discounts.push(discounts.item2Fixed) + res = totalsService.getDiscountTotal(discountCart) expect(res).toEqual(20) }) it("calculate item percentage discount", async () => { - carts.discountCart.discounts.push(discounts.item10Percent) - res = await totalsService.getDiscountTotal(carts.discountCart) + discountCart.discounts.push(discounts.item10Percent) + res = totalsService.getDiscountTotal(discountCart) expect(res).toEqual(10) }) it("calculate total fixed discount", async () => { - carts.discountCart.discounts.push(discounts.total10Fixed) - res = await totalsService.getDiscountTotal(carts.discountCart) + discountCart.discounts.push(discounts.total10Fixed) + res = totalsService.getDiscountTotal(discountCart) expect(res).toEqual(10) }) it("ignores discount if expired", async () => { - carts.discountCart.discounts.push(discounts.expiredDiscount) - res = await totalsService.getDiscountTotal(carts.discountCart) + discountCart.discounts.push(discounts.expiredDiscount) + res = totalsService.getDiscountTotal(discountCart) expect(res).toEqual(0) }) it("returns 0 if no discounts are applied", async () => { - res = await totalsService.getDiscountTotal(carts.discountCart) + res = totalsService.getDiscountTotal(discountCart) expect(res).toEqual(0) }) it("returns 0 if no items are in cart", async () => { - res = await totalsService.getDiscountTotal(carts.regionCart) + res = totalsService.getDiscountTotal({ + items: [], + discounts: [discounts.total10Fixed], + }) expect(res).toEqual(0) }) @@ -150,58 +275,86 @@ describe("TotalsService", () => { describe("getRefundTotal", () => { let res - const totalsService = new TotalsService({ - productVariantService: ProductVariantServiceMock, - regionService: RegionServiceMock, - }) + const totalsService = new TotalsService() + const orderToRefund = { + id: "refund-order", + tax_rate: 25, + items: [ + { + id: "line", + unit_price: 100, + allow_discounts: true, + variant: { + id: "variant", + product_id: "testp1", + }, + quantity: 10, + returned_quantity: 0, + }, + { + id: "line2", + unit_price: 100, + allow_discounts: true, + variant: { + id: "variant", + product_id: "testp2", + }, + quantity: 10, + returned_quantity: 0, + metadata: {}, + }, + { + id: "non-discount", + unit_price: 100, + allow_discounts: false, + variant: { + id: "variant", + product_id: "testp2", + }, + quantity: 1, + returned_quantity: 0, + metadata: {}, + }, + ], + region_id: "fr", + discounts: [], + } beforeEach(() => { jest.clearAllMocks() - orders.orderToRefund.discounts = [] + orderToRefund.discounts = [] }) it("calculates refund", async () => { - res = await totalsService.getRefundTotal(orders.orderToRefund, [ + res = totalsService.getRefundTotal(orderToRefund, [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "line2", + unit_price: 100, + variant: { + id: "variant", + product_id: "product2", }, quantity: 10, + returned_quantity: 0, + metadata: {}, }, ]) - expect(res).toEqual(1537.5) + expect(res).toEqual(1250) }) it("calculates refund with total precentage discount", async () => { - orders.orderToRefund.discounts.push(discounts.total10Percent) - res = await totalsService.getRefundTotal(orders.orderToRefund, [ + orderToRefund.discounts.push(discounts.total10Percent) + res = totalsService.getRefundTotal(orderToRefund, [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "line2", + unit_price: 100, + variant: { + id: "variant", + product_id: "product2", }, + returned_quantity: 0, + metadata: {}, quantity: 10, }, ]) @@ -210,155 +363,177 @@ describe("TotalsService", () => { }) it("calculates refund with total fixed discount", async () => { - orders.orderToRefund.discounts.push(discounts.total10Fixed) - res = await totalsService.getRefundTotal(orders.orderToRefund, [ + orderToRefund.discounts.push(discounts.total10Fixed) + res = totalsService.getRefundTotal(orderToRefund, [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "line", + unit_price: 100, + variant: { + id: "variant", + product_id: "product", }, - quantity: 3, + quantity: 10, + returned_quantity: 0, }, ]) - expect(res).toEqual(373.125) + expect(res).toEqual(1243.75) }) it("calculates refund with item fixed discount", async () => { - orders.orderToRefund.discounts.push(discounts.item2Fixed) - res = await totalsService.getRefundTotal(orders.orderToRefund, [ + orderToRefund.discounts.push(discounts.item2Fixed) + res = totalsService.getRefundTotal(orderToRefund, [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "line2", + unit_price: 100, + variant: { + id: "variant", + product_id: "testp2", }, - quantity: 3, + quantity: 10, + returned_quantity: 0, }, ]) - expect(res).toEqual(367.5) + expect(res).toEqual(1225) }) it("calculates refund with item percentage discount", async () => { - orders.orderToRefund.discounts.push(discounts.item10Percent) - res = await totalsService.getRefundTotal(orders.orderToRefund, [ + orderToRefund.discounts.push(discounts.item10Percent) + res = totalsService.getRefundTotal(orderToRefund, [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "line2", + unit_price: 100, + variant: { + id: "variant", + product_id: "testp2", }, - quantity: 3, + quantity: 10, + returned_quantity: 0, }, ]) - expect(res).toEqual(337.5) + expect(res).toEqual(1125) }) it("throws if line items to return is not in order", async () => { - try { - await totalsService.getRefundTotal(orders.orderToRefund, [ + const work = () => + totalsService.getRefundTotal(orderToRefund, [ { - _id: IdMap.getId("notInOrder"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, + id: "notInOrder", + unit_price: 123, + variant: { + id: "variant", + product_id: "pid", }, - quantity: 3, + quantity: 1, }, ]) - } catch (error) { - expect(error.message).toEqual("Line items does not exist on order") - } + + expect(work).toThrow("Line item does not exist on order") }) }) describe("getShippingTotal", () => { let res - const totalsService = new TotalsService({ - productVariantService: ProductVariantServiceMock, - }) + const totalsService = new TotalsService() beforeEach(() => { jest.clearAllMocks() }) it("calculates shipping", async () => { - res = await totalsService.getShippingTotal(orders.testOrder) + const order = { + shipping_methods: [ + { + _id: IdMap.getId("expensiveShipping"), + name: "Expensive Shipping", + price: 100, + provider_id: "default_provider", + profile_id: IdMap.getId("default"), + data: { + extra: "hi", + }, + }, + ], + } + res = totalsService.getShippingTotal(order) expect(res).toEqual(100) }) }) describe("getTaxTotal", () => { let res - const totalsService = new TotalsService({ - productVariantService: ProductVariantServiceMock, - regionService: RegionServiceMock, - }) + const totalsService = new TotalsService() beforeEach(() => { jest.clearAllMocks() - orders.orderToRefund.discounts = [] }) it("calculates tax", async () => { - res = await totalsService.getTaxTotal(orders.testOrder) + const order = { + region: { + tax_rate: 25, + }, + items: [ + { + unit_price: 20, + quantity: 2, + }, + ], + shipping_methods: [ + { + _id: IdMap.getId("expensiveShipping"), + name: "Expensive Shipping", + price: 100, + provider_id: "default_provider", + profile_id: IdMap.getId("default"), + data: { + extra: "hi", + }, + }, + ], + } - expect(res).toEqual(332.5) + res = totalsService.getTaxTotal(order) + + expect(res).toEqual(35) }) }) describe("getTotal", () => { let res - const totalsService = new TotalsService({ - productVariantService: ProductVariantServiceMock, - regionService: RegionServiceMock, - }) + const totalsService = new TotalsService() beforeEach(() => { jest.clearAllMocks() }) it("calculates total", async () => { - res = await totalsService.getTotal(orders.testOrder) - expect(res).toEqual(1230 + 332.5 + 100) + const order = { + region: { + tax_rate: 25, + }, + items: [ + { + unit_price: 20, + quantity: 2, + }, + ], + shipping_methods: [ + { + _id: IdMap.getId("expensiveShipping"), + name: "Expensive Shipping", + price: 100, + provider_id: "default_provider", + profile_id: IdMap.getId("default"), + data: { + extra: "hi", + }, + }, + ], + } + res = totalsService.getTotal(order) + expect(res).toEqual(175) }) }) }) diff --git a/packages/medusa/src/services/__tests__/user.js b/packages/medusa/src/services/__tests__/user.js index 6e4867fe0b..4c83ba76f7 100644 --- a/packages/medusa/src/services/__tests__/user.js +++ b/packages/medusa/src/services/__tests__/user.js @@ -1,47 +1,53 @@ -import mongoose from "mongoose" -import jwt from "jsonwebtoken" -import { IdMap } from "medusa-test-utils" +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import UserService from "../user" -import { UserModelMock, users } from "../../models/__mocks__/user" -import { EventBusServiceMock } from "../__mocks__/event-bus" + +const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, +} describe("UserService", () => { describe("retrieve", () => { - let result + const userRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + }) + const userService = new UserService({ + manager: MockManager, + userRepository, + }) - beforeAll(async () => { + beforeEach(async () => { jest.clearAllMocks() - const userService = new UserService({ - userModel: UserModelMock, - }) - result = await userService.retrieve(IdMap.getId("test-user")) }) - it("calls user model functions", () => { - expect(UserModelMock.findOne).toHaveBeenCalledTimes(1) - expect(UserModelMock.findOne).toHaveBeenCalledWith({ - _id: IdMap.getId("test-user"), - }) - }) + it("successfully retrieves a user", async () => { + const result = await userService.retrieve(IdMap.getId("ironman")) - it("returns the user", () => { - expect(result).toEqual(users.testUser) + expect(userRepository.findOne).toHaveBeenCalledTimes(1) + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("ironman") }, + }) + + expect(result.id).toEqual(IdMap.getId("ironman")) }) }) describe("create", () => { - let result + const userRepository = MockRepository({}) const userService = new UserService({ - userModel: UserModelMock, + manager: MockManager, + userRepository, }) - beforeAll(async () => { + beforeEach(async () => { jest.clearAllMocks() }) - it("calls user model functions", async () => { - result = await userService.create( + it("successfully create a user", async () => { + await userService.create( { email: "oliver@test.dk", name: "Oliver", @@ -49,8 +55,9 @@ describe("UserService", () => { }, "password" ) - expect(UserModelMock.create).toHaveBeenCalledTimes(1) - expect(UserModelMock.create).toHaveBeenCalledWith({ + + expect(userRepository.create).toHaveBeenCalledTimes(1) + expect(userRepository.create).toHaveBeenCalledWith({ email: "oliver@test.dk", name: "Oliver", password_hash: expect.stringMatching(/.{128}$/), @@ -59,99 +66,90 @@ describe("UserService", () => { }) describe("update", () => { + const userRepository = MockRepository({ + findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + }) const userService = new UserService({ - userModel: UserModelMock, + manager: MockManager, + userRepository, }) - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks() }) - it("calls updateOne with correct params", async () => { - await userService.update(IdMap.getId("test-user"), { - name: "new name", + it("successfully updates user", async () => { + await userService.update(IdMap.getId("ironman"), { + first_name: "Tony", + last_name: "Stark", }) - expect(UserModelMock.updateOne).toBeCalledTimes(1) - expect(UserModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("test-user") }, - { $set: { name: "new name" } }, - { runValidators: true } - ) - }) - }) - - describe("setMetadata", () => { - const userService = new UserService({ - userModel: UserModelMock, + expect(userRepository.save).toBeCalledTimes(1) + expect(userRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + first_name: "Tony", + last_name: "Stark", + }) }) - beforeEach(() => { - jest.clearAllMocks() + it("successfully updates user metadata", async () => { + await userService.update(IdMap.getId("ironman"), { + metadata: { + company: "Stark Industries", + }, + }) + + expect(userRepository.save).toBeCalledTimes(1) + expect(userRepository.save).toBeCalledWith({ + id: IdMap.getId("ironman"), + metadata: { + company: "Stark Industries", + }, + }) }) - it("calls updateOne with correct params", async () => { - const id = mongoose.Types.ObjectId() - await userService.setMetadata(`${id}`, "metadata", "testMetadata") - - expect(UserModelMock.updateOne).toBeCalledTimes(1) - expect(UserModelMock.updateOne).toBeCalledWith( - { _id: `${id}` }, - { $set: { "metadata.metadata": "testMetadata" } } - ) - }) - - it("throw error on invalid key type", async () => { - const id = mongoose.Types.ObjectId() - + it("fails on email update", async () => { try { - await userService.setMetadata(`${id}`, 1234, "nono") - } catch (err) { - expect(err.message).toEqual( - "Key type is invalid. Metadata keys must be strings" + await userService.update(IdMap.getId("ironman"), { + email: "tony@stark.com", + }) + } catch (error) { + expect(error.message).toBe("You are not allowed to update email") + } + }) + + it("fails on password update", async () => { + try { + await userService.update(IdMap.getId("ironman"), { + password_hash: "lol", + }) + } catch (error) { + expect(error.message).toBe( + "Use dedicated methods, `setPassword`, `generateResetPasswordToken` for password operations" ) } }) }) - describe("setPassword", () => { - const userService = new UserService({ - userModel: UserModelMock, - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("calls updateOne with correct params", async () => { - await userService.setPassword(IdMap.getId("test-user"), "123456789") - expect(UserModelMock.updateOne).toBeCalledTimes(1) - expect(UserModelMock.updateOne).toBeCalledWith( - { _id: IdMap.getId("test-user") }, - { - $set: { - // Since bcrypt hashing always varies, we are testing the password - // match by using a regular expression. - password_hash: expect.stringMatching(/^.{128}$/), - }, - } - ) - }) - }) - describe("generateResetPasswordToken", () => { - const userService = new UserService({ - eventBusService: EventBusServiceMock, - userModel: UserModelMock, + const userRepository = MockRepository({ + findOne: () => + Promise.resolve({ id: IdMap.getId("ironman"), password_hash: "lol" }), }) - beforeEach(() => { + const userService = new UserService({ + manager: MockManager, + userRepository, + eventBusService, + }) + + beforeEach(async () => { jest.clearAllMocks() }) it("generates a token successfully", async () => { const token = await userService.generateResetPasswordToken( - IdMap.getId("test-user") + IdMap.getId("ironman") ) expect(token).toMatch( diff --git a/packages/medusa/src/services/auth.js b/packages/medusa/src/services/auth.js index b97559228c..724cf012ad 100644 --- a/packages/medusa/src/services/auth.js +++ b/packages/medusa/src/services/auth.js @@ -83,6 +83,7 @@ class AuthService extends BaseService { } } } catch (error) { + console.log(error) return { success: false, error: "Invalid email or password", diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index 405dd501c0..aa9608ec80 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -1,6 +1,7 @@ import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { defaultFields, defaultRelations } from "../api/routes/store/carts" /** * Provides layer to manipulate carts. @@ -14,7 +15,9 @@ class CartService extends BaseService { } constructor({ - cartModel, + manager, + cartRepository, + shippingMethodRepository, eventBusService, paymentProviderService, productService, @@ -25,12 +28,21 @@ class CartService extends BaseService { shippingProfileService, customerService, discountService, + giftCardService, totalsService, + addressRepository, + paymentSessionRepository, }) { super() - /** @private @const {CartModel} */ - this.cartModel_ = cartModel + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {ShippingMethodRepository} */ + this.shippingMethodRepository_ = shippingMethodRepository + + /** @private @const {CartRepository} */ + this.cartRepository_ = cartRepository /** @private @const {EventBus} */ this.eventBus_ = eventBusService @@ -62,8 +74,47 @@ class CartService extends BaseService { /** @private @const {DiscountService} */ this.discountService_ = discountService - /** @private @const {DiscountService} */ + /** @private @const {GiftCardService} */ + this.giftCardService_ = giftCardService + + /** @private @const {TotalsService} */ this.totalsService_ = totalsService + + /** @private @const {AddressRepository} */ + this.addressRepository_ = addressRepository + + /** @private @const {PaymentSessionRepository} */ + this.paymentSessionRepository_ = paymentSessionRepository + } + + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new CartService({ + manager: transactionManager, + cartRepository: this.cartRepository_, + eventBusService: this.eventBus_, + paymentProviderService: this.paymentProviderService_, + paymentSessionRepository: this.paymentSessionRepository_, + shippingMethodRepository: this.shippingMethodRepository_, + productService: this.productService_, + productVariantService: this.productVariantService_, + regionService: this.regionService_, + lineItemService: this.lineItemService_, + shippingOptionService: this.shippingOptionService_, + shippingProfileService: this.shippingProfileService_, + customerService: this.customerService_, + discountService: this.discountService_, + totalsService: this.totalsService_, + addressRepository: this.addressRepository_, + giftCardService: this.giftCardService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -71,19 +122,6 @@ class CartService extends BaseService { * @param {string} rawId - the raw cart id to validate. * @return {string} the validated id */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The cartId could not be casted to an ObjectId" - ) - } - - return value - } - /** * Contents of a line item * @typedef {(object | array)} LineItemContent @@ -110,65 +148,100 @@ class CartService extends BaseService { * @param {number} - the quantity of the line item * @return {boolean} true if the inventory covers the line item. */ - async confirmInventory_(content, lineQuantity) { - if (Array.isArray(content)) { - const coverage = await Promise.all( - content.map(({ variant, quantity }) => { - return this.productVariantService_.canCoverQuantity( - variant._id, - lineQuantity * quantity - ) - }) - ) - - return coverage.every(c => c) + async confirmInventory_(variantId, quantity) { + // If the line item is not stock tracked we don't have double check it + if (!variantId) { + return true } - const { variant, quantity } = content - return this.productVariantService_.canCoverQuantity( - variant._id, - lineQuantity * quantity - ) + return this.productVariantService_.canCoverQuantity(variantId, quantity) } - /** - * Transforms some line item content to have unit_prices corresponding to a - * given region's pricing scheme. - * @param {(LineItemContent | LineItemContentArray)} - the content of the line - * item - * @param {string} regionId - the id of the region whose price we should - * update to - * @return {(LineItemContent | LineItemContentArray)} true if the inventory - * covers the line item. - */ - async updateContentPrice_(content, regionId) { - if (Array.isArray(content)) { - return await Promise.all( - content.map(async c => { - const unitPrice = await this.productVariantService_.getRegionPrice( - c.variant._id, - regionId - ) - c.unit_price = unitPrice - return c - }) - ) + transformQueryForTotals_(config) { + let { select, relations } = config + + if (!select) { + return { + select, + relations, + totalsToSelect: [], + } } - const unitPrice = await this.productVariantService_.getRegionPrice( - content.variant._id, - regionId - ) - content.unit_price = unitPrice - return content + const totalFields = [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "gift_card_total", + "total", + ] + + const totalsToSelect = select.filter(v => totalFields.includes(v)) + if (totalsToSelect.length > 0) { + const relationSet = new Set(relations) + relationSet.add("items") + relationSet.add("gift_cards") + relationSet.add("discounts") + //relationSet.add("discounts.parent_discount") + //relationSet.add("discounts.parent_discount.rule") + //relationSet.add("discounts.parent_discount.regions") + relationSet.add("shipping_methods") + relationSet.add("region") + relations = [...relationSet] + + select = select.filter(v => !totalFields.includes(v)) + } + + return { + relations, + select, + totalsToSelect, + } + } + + async decorateTotals_(cart, totalsFields = []) { + if (totalsFields.includes("shipping_total")) { + cart.shipping_total = await this.totalsService_.getShippingTotal(cart) + } + if (totalsFields.includes("discount_total")) { + cart.discount_total = await this.totalsService_.getDiscountTotal(cart) + } + if (totalsFields.includes("tax_total")) { + cart.tax_total = await this.totalsService_.getTaxTotal(cart) + } + if (totalsFields.includes("gift_card_total")) { + cart.gift_card_total = await this.totalsService_.getGiftCardTotal(cart) + } + if (totalsFields.includes("subtotal")) { + cart.subtotal = await this.totalsService_.getSubtotal(cart) + } + if (totalsFields.includes("total")) { + cart.total = await this.totalsService_.getTotal(cart) + } + return cart } /** * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector) { - return this.cartModel_.find(selector) + list(selector, config = {}) { + const cartRepo = this.manager_.getCustomRepository(this.cartRepository_) + + const query = { + where: selector, + } + + if (config.select) { + query.select = config.select + } + + if (config.relations) { + query.relations = config.relations + } + + return cartRepo.find(query) } /** @@ -176,20 +249,36 @@ class CartService extends BaseService { * @param {string} cartId - the id of the cart to get. * @return {Promise} the cart document. */ - async retrieve(cartId) { + async retrieve(cartId, options = {}) { + const cartRepo = this.manager_.getCustomRepository(this.cartRepository_) const validatedId = this.validateId_(cartId) - const cart = await this.cartModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - if (!cart) { + const { select, relations, totalsToSelect } = this.transformQueryForTotals_( + options + ) + + const query = { + where: { id: validatedId }, + } + + if (relations && relations.length > 0) { + query.relations = relations + } + + if (select && select.length > 0) { + query.select = select + } + + const raw = await cartRepo.findOne(query) + + if (!raw) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Cart with ${cartId} was not found` ) } + + const cart = await this.decorateTotals_(raw, totalsToSelect) return cart } @@ -199,84 +288,65 @@ class CartService extends BaseService { * @return {Promise} the result of the create operation */ async create(data) { - const { region_id } = data - if (!region_id) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `A region_id must be provided when creating a cart` - ) - } - - const region = await this.regionService_.retrieve(region_id) - if (!data.shipping_address) { - if (region.countries.length === 1) { - // Preselect the country if the region only has 1 - data.shipping_address = { - country_code: region.countries[0], - } - } - } else { - if (!region.countries.includes(data.shipping_address.country_code)) { + return this.atomicPhase_(async manager => { + const cartRepo = manager.getCustomRepository(this.cartRepository_) + const addressRepo = manager.getCustomRepository(this.addressRepository_) + const { region_id } = data + if (!region_id) { throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Shipping country not in region" + MedusaError.Types.INVALID_DATA, + `A region_id must be provided when creating a cart` ) } - } - return this.cartModel_ - .create({ - ...data, - region_id: region._id, + const region = await this.regionService_.retrieve(region_id, { + relations: ["countries"], }) - .then(result => { - this.eventBus_.emit(CartService.Events.CREATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - /** - * Decorates a cart. - * @param {Cart} cart - the cart to decorate. - * @param {string[]} fields - the fields to include. - * @param {string[]} expandFields - fields to expand. - * @return {Cart} return the decorated cart. - */ - async decorate(cart, fields, expandFields = []) { - const c = cart - c.shipping_total = await this.totalsService_.getShippingTotal(cart) - c.discount_total = await this.totalsService_.getDiscountTotal(cart) - c.tax_total = await this.totalsService_.getTaxTotal(cart) - c.subtotal = await this.totalsService_.getSubtotal(cart) - c.total = await this.totalsService_.getTotal(cart) - if (expandFields.includes("region")) { - c.region = await this.regionService_.retrieve(cart.region_id) - } + const regCountries = region.countries.map(({ iso_2 }) => iso_2) - const final = await this.runDecorators_(c) - return final - } - - /** - * Returns an array of product ids in a line item. - * @param {LineItem} item - the line item to fetch products from - * @return {[string]} an array of product ids - */ - getItemProducts_(item) { - // Find all the products in the line item - const products = [] - if (Array.isArray(item.content)) { - item.content.forEach(c => products.push(`${c.product._id}`)) - } else { - if (item.content.product) { - products.push(`${item.content.product._id}`) + if (!data.shipping_address && !data.shipping_address_id) { + if (region.countries.length === 1) { + // Preselect the country if the region only has 1 + // and create address entity + data.shipping_address = addressRepo.create({ + country_code: regCountries[0], + }) + } + } else { + if (data.shipping_address) { + if (!regCountries.includes(data.shipping_address.country_code)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Shipping country not in region" + ) + } + } + if (data.shipping_address_id) { + const addr = await addressRepo.findOne(data.shipping_address_id) + if (!regCountries.includes(addr.country_code)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Shipping country not in region" + ) + } + } } - } - return products + const toCreate = { + ...data, + region_id: region.id, + } + + const inProgress = await cartRepo.create(toCreate) + const result = await cartRepo.save(inProgress) + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.CREATED, { + id: result.id, + }) + return result + }) } /** @@ -286,58 +356,47 @@ class CartService extends BaseService { * @retur {Promise} the result of the update operation */ async removeLineItem(cartId, lineItemId) { - const cart = await this.retrieve(cartId) - const itemToRemove = cart.items.find(line => line._id.equals(lineItemId)) - if (!itemToRemove) { - return Promise.resolve(cart) - } + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: [ + "items", + "items.variant", + "items.variant.product", + "payment_sessions", + ], + }) - const update = { - $pull: { items: { _id: itemToRemove._id } }, - } + const lineItem = cart.items.find(li => li.id === lineItemId) + if (!lineItem) { + return cart + } - // Remove shipping methods if they are not needed - if (cart.shipping_methods && cart.shipping_methods.length) { - const filteredItems = cart.items.filter(i => !i._id.equals(lineItemId)) - - let newShippingMethods = await Promise.all( - cart.shipping_methods.map(async m => { - const profile = await this.shippingProfileService_.retrieve( - m.profile_id - ) - const hasItem = filteredItems.find(item => { - const products = this.getItemProducts_(item) - return products.some(p => profile.products.includes(p)) - }) - - if (hasItem) { - return m - } - - return null - }) - ) - newShippingMethods = newShippingMethods.filter(n => !!n) - - if (newShippingMethods.length !== cart.shipping_methods.length) { - update.$set = { - shipping_methods: newShippingMethods, + // Remove shipping methods if they are not needed + if (cart.shipping_methods && cart.shipping_methods.length) { + for (const method of cart.shipping_methods) { + await this.shippingOptionService_ + .withTransaction(manager) + .deleteShippingMethod(method) } } - } - return this.cartModel_ - .updateOne( - { - _id: cartId, - }, - update - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) + for (const itm of cart.items) { + await this.lineItemService_.withTransaction(manager).update(itm.id, { + has_shipping: false, + }) + } + + await this.lineItemService_.withTransaction(manager).delete(lineItem.id) + + const result = await this.retrieve(cartId) + // Notify subscribers + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, { + id: result.id, + }) + return result + }) } /** @@ -348,21 +407,22 @@ class CartService extends BaseService { * @param {LineItem} lineItem - the line item * @return {boolean} */ - async validateLineItemShipping_(shippingMethods, lineItem) { - if (shippingMethods && shippingMethods.length) { - const profiles = await Promise.all( - shippingMethods.map(m => - this.shippingProfileService_.retrieve(m.profile_id) - ) - ) + validateLineItemShipping_(shippingMethods, lineItem) { + if (!lineItem.variant_id) { + return true + } - const products = this.getItemProducts_(lineItem) - - // Check if there is a shipping method for each product - const hasShipping = products.map( - p => !!profiles.find(profile => profile.products.includes(p)) + if ( + shippingMethods && + shippingMethods.length && + lineItem.variant && + lineItem.variant.product + ) { + const productProfile = lineItem.variant.product.profile_id + const selectedProfiles = shippingMethods.map( + ({ shipping_option }) => shipping_option.profile_id ) - return hasShipping.every(b => b) + return selectedProfiles.includes(productProfile) } return false @@ -372,184 +432,242 @@ class CartService extends BaseService { * Adds a line item to the cart. * @param {string} cartId - the id of the cart that we will add to * @param {LineItem} lineItem - the line item to add. - * @retur {Promise} the result of the update operation + * @return {Promise} the result of the update operation */ async addLineItem(cartId, lineItem) { - const validatedLineItem = this.lineItemService_.validate(lineItem) - const cart = await this.retrieve(cartId) - const currentItem = cart.items.find(line => - this.lineItemService_.isEqual(line, validatedLineItem) - ) + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: [ + "shipping_methods", + "items", + "payment_sessions", + "items.variant", + "items.variant.product", + ], + }) - const hasShipping = await this.validateLineItemShipping_( - cart.shipping_methods, - validatedLineItem - ) - - // If content matches one of the line items currently in the cart we can - // simply update the quantity of the existing line item - if (currentItem && validatedLineItem.should_merge) { - const newQuantity = currentItem.quantity + validatedLineItem.quantity - - // Confirm inventory - const hasInventory = await this.confirmInventory_( - validatedLineItem.content, - newQuantity - ) - - if (!hasInventory) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Inventory doesn't cover the desired quantity" - ) + let currentItem + if (lineItem.should_merge) { + currentItem = cart.items.find(line => { + if (line.should_merge && line.variant_id === lineItem.variant_id) { + return _.isEqual(line.metadata, lineItem.metadata) + } + }) } - return this.cartModel_ - .updateOne( - { - _id: cartId, - "items._id": currentItem._id, - }, - { - $set: { - "items.$.quantity": newQuantity, - "items.$.has_shipping": hasShipping, - }, - } + // If content matches one of the line items currently in the cart we can + // simply update the quantity of the existing line item + if (currentItem) { + const newQuantity = currentItem.quantity + lineItem.quantity + + // Confirm inventory + const hasInventory = await this.confirmInventory_( + lineItem.variant_id, + newQuantity ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } - // Confirm inventory - const hasInventory = await this.confirmInventory_( - validatedLineItem.content, - validatedLineItem.quantity - ) - - if (!hasInventory) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Inventory doesn't cover the desired quantity" - ) - } - - // The line we are adding doesn't already exist so it is safe to push - return this.cartModel_ - .updateOne( - { - _id: cartId, - }, - { - $push: { - items: { - ...validatedLineItem, - has_shipping: hasShipping, - }, - }, + if (!hasInventory) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Inventory doesn't cover the desired quantity" + ) } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) + + await this.lineItemService_ + .withTransaction(manager) + .update(currentItem.id, { + quantity: newQuantity, + }) + } else { + // Confirm inventory + const hasInventory = await this.confirmInventory_( + lineItem.variant_id, + lineItem.quantity + ) + + if (!hasInventory) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Inventory doesn't cover the desired quantity" + ) + } + + await this.lineItemService_.withTransaction(manager).create({ + ...lineItem, + has_shipping: false, + cart_id: cartId, + }) + } + + for (const itm of cart.items) { + await this.lineItemService_.withTransaction(manager).update(itm.id, { + has_shipping: false, + }) + } + + // Remove shipping methods + if (cart.shipping_methods && cart.shipping_methods.length) { + for (const method of cart.shipping_methods) { + await this.shippingOptionService_ + .withTransaction(manager) + .deleteShippingMethod(method) + } + } + + const result = await this.retrieve(cartId) + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }) } /** * Updates a cart's existing line item. * @param {string} cartId - the id of the cart to update * @param {string} lineItemId - the id of the line item to update. - * @param {LineItem} lineItem - the line item to update. Must include an _id + * @param {LineItemUpdate} lineItem - the line item to update. Must include an id * field. * @return {Promise} the result of the update operation */ - async updateLineItem(cartId, lineItemId, lineItem) { - const cart = await this.retrieve(cartId) - const validatedLineItem = this.lineItemService_.validate(lineItem) - - if (!lineItemId) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Line Item must have an _id corresponding to an existing line item id" - ) - } - - // Ensure that the line item exists in the cart - const lineItemExists = cart.items.find(i => i._id.equals(lineItemId)) - if (!lineItemExists) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "A line item with the provided id doesn't exist in the cart" - ) - } - - // Ensure that inventory covers the request - const hasInventory = await this.confirmInventory_( - validatedLineItem.content, - validatedLineItem.quantity - ) - - if (!hasInventory) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Inventory doesn't cover the desired quantity" - ) - } - - // Update the line item - return this.cartModel_ - .updateOne( - { - _id: cartId, - "items._id": lineItemId, - }, - { - $set: { - "items.$": validatedLineItem, - }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + async updateLineItem(cartId, lineItemId, lineItemUpdate) { + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: ["items", "payment_sessions"], }) + + // Ensure that the line item exists in the cart + const lineItemExists = cart.items.find(i => i.id === lineItemId) + if (!lineItemExists) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "A line item with the provided id doesn't exist in the cart" + ) + } + + if (lineItemUpdate.quantity) { + const hasInventory = await this.confirmInventory_( + lineItemExists.variant_id, + lineItemUpdate.quantity + ) + + if (!hasInventory) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Inventory doesn't cover the desired quantity" + ) + } + } + + await this.lineItemService_ + .withTransaction(manager) + .update(lineItemId, lineItemUpdate) + + // Update the line item + const result = await this.retrieve(cartId) + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }) } + + async update(cartId, update) { + return this.atomicPhase_(async manager => { + const cartRepo = manager.getCustomRepository(this.cartRepository_) + const cart = await this.retrieve(cartId, { + select: [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "total", + ], + relations: [ + "items", + "shipping_methods", + "shipping_address", + "billing_address", + "gift_cards", + "discounts", + "customer", + "region", + "payment_sessions", + "region.countries", + "discounts.rule", + "discounts.regions", + ], + }) + + if ("region_id" in update) { + await this.setRegion_(cart, update.region_id, update.country_code) + } + + if ("customer_id" in update) { + await this.updateCustomerId_(cart, update.customer_id) + } else { + if ("email" in update) { + await this.updateEmail_(cart, update.email) + } + } + + const addrRepo = manager.getCustomRepository(this.addressRepository_) + if ("shipping_address" in update) { + await this.updateShippingAddress_( + cart, + update.shipping_address, + addrRepo + ) + } + + if ("billing_address" in update) { + await this.updateBillingAddress_(cart, update.billing_address, addrRepo) + } + + if ("discounts" in update) { + cart.discounts = [] + for (const { code } of update.discounts) { + await this.applyDiscount_(cart, code) + } + } + + if ("gift_cards" in update) { + cart.gift_cards = [] + + for (const { code } of update.gift_cards) { + await this.applyGiftCard_(cart, code) + } + } + + const result = await cartRepo.save(cart) + + if ("email" in update || "customer_id" in update) { + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.CUSTOMER_UPDATED, result.id) + } + + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + + return result + }) + } + /** * Sets the customer id of a cart * @param {string} cartId - the id of the cart to add email to * @param {string} customerId - the customer to add to cart * @return {Promise} the result of the update operation */ - async updateCustomerId(cartId, customerId) { - const cart = await this.retrieve(cartId) - const schema = Validator.objectId().required() - const { value, error } = schema.validate(customerId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The customerId is not valid" - ) - } + async updateCustomerId_(cart, customerId) { + const customer = await this.customerService_ + .withTransaction(this.transactionManager_) + .retrieve(customerId) - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { customer_id: value }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.CUSTOMER_UPDATED, result) - return result - }) + cart.customer_id = customer.id + cart.email = customer.email } /** @@ -558,12 +676,11 @@ class CartService extends BaseService { * @param {string} email - the email to add to cart * @return {Promise} the result of the update operation */ - async updateEmail(cartId, email) { - const cart = await this.retrieve(cartId) + async updateEmail_(cart, email) { const schema = Validator.string() .email() .required() - const { value, error } = schema.validate(email) + const { value, error } = schema.validate(email.toLowerCase()) if (error) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -573,34 +690,17 @@ class CartService extends BaseService { let customer = await this.customerService_ .retrieveByEmail(value) - .catch(err => undefined) + .catch(() => undefined) if (!customer) { - customer = await this.customerService_.create({ email }) + customer = await this.customerService_ + .withTransaction(this.manager_) + .create({ email }) } - const customerChanged = !customer._id.equals(cart.customer_id) - - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { - email: value, - customer_id: customer._id, - }, - } - ) - .then(result => { - // Notify subscribers - if (customerChanged) { - this.eventBus_.emit(CartService.Events.CUSTOMER_UPDATED, result) - } - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) + cart.email = value + cart.customer = customer + cart.customer_id = customer.id } /** @@ -609,29 +709,21 @@ class CartService extends BaseService { * @param {object} address - the value to set the billing address to * @return {Promise} the result of the update operation */ - async updateBillingAddress(cartId, address) { - const cart = await this.retrieve(cartId) - const { value, error } = Validator.address().validate(address) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.message) - } - - address.country_code = address.country_code.toUpperCase() - - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { billing_address: value }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + async updateBillingAddress_(cart, address, addrRepo) { + address.country_code = address.country_code.toLowerCase() + if (cart.billing_address_id) { + const addr = await addrRepo.findOne({ + where: { id: cart.billing_address_id }, }) + + await addrRepo.save({ ...addr, ...address }) + } else { + const created = addrRepo.create({ + ...address, + }) + + cart.billing_address = created + } } /** @@ -640,38 +732,58 @@ class CartService extends BaseService { * @param {object} address - the value to set the shipping address to * @return {Promise} the result of the update operation */ - async updateShippingAddress(cartId, address) { - const cart = await this.retrieve(cartId) - const { value, error } = Validator.address().validate(address) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.message) - } + async updateShippingAddress_(cart, address, addrRepo) { + address.country_code = address.country_code.toLowerCase() - address.country_code = address.country_code.toUpperCase() - - const region = await this.regionService_.retrieve(cart.region_id) - if (!region.countries.includes(address.country_code.toUpperCase())) { + if ( + !cart.region.countries.find(({ iso_2 }) => address.country_code === iso_2) + ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Shipping country must be in the cart region" ) } - return this.cartModel_ - .updateOne( - { - _id: cartId, - }, - { - $set: { shipping_address: value }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + if (cart.shipping_address_id) { + const addr = await addrRepo.findOne({ + where: { id: cart.shipping_address_id }, }) + + await addrRepo.save({ ...addr, ...address }) + } else { + const created = addrRepo.create({ + ...address, + }) + + cart.shipping_address = created + } } + + async applyGiftCard_(cart, code) { + const giftCard = await this.giftCardService_.retrieveByCode(code) + + if (giftCard.is_disabled) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "The gift card is disabled" + ) + } + + if (giftCard.region_id !== cart.region_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The gift card cannot be used in the current region" + ) + } + + // if discount is already there, we simply resolve + if (cart.gift_cards.find(({ id }) => id === giftCard.id)) { + return Promise.resolve() + } + + cart.gift_cards = [...cart.gift_cards, giftCard] + } + /** * Updates the cart's discounts. * If discount besides free shipping is already applied, this @@ -681,18 +793,33 @@ class CartService extends BaseService { * @param {string} discountCode - the discount code * @return {Promise} the result of the update operation */ - async applyDiscount(cartId, discountCode) { - const cart = await this.retrieve(cartId) - const discount = await this.discountService_.retrieveByCode(discountCode) + async applyDiscount_(cart, discountCode) { + const discount = await this.discountService_.retrieveByCode(discountCode, [ + "rule", + "regions", + ]) - if (discount.disabled) { + const rule = discount.rule + let regions = discount.regions + if (discount.parent_discount_id) { + const parent = await this.discountService_.retrieve( + discount.parent_discount_id, + { + relations: ["rule", "regions"], + } + ) + + regions = parent.regions + } + + if (discount.is_disabled) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "The discount code is disabled" ) } - if (!discount.regions.includes(cart.region_id)) { + if (!regions.find(({ id }) => id === cart.region_id)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "The discount is not available in current region" @@ -700,101 +827,63 @@ class CartService extends BaseService { } // if discount is already there, we simply resolve - if (cart.discounts.includes(discount._id)) { + if (cart.discounts.find(({ id }) => id === discount.id)) { return Promise.resolve() } - // find the current discounts (if there) - // partition them into shipping and other - const [shippingDisc, otherDisc] = _.partition( - cart.discounts, - d => d.discount_rule.type === "free_shipping" - ) + const toParse = [...cart.discounts, discount] - // if no shipping exists and the one to apply is shipping, we simply add it - // else we remove the current shipping and add the other one - if ( - shippingDisc.length === 0 && - discount.discount_rule.type === "free_shipping" - ) { - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $push: { discounts: discount }, + let sawNotShipping = false + const newDiscounts = toParse.map(d => { + const drule = d.rule + switch (drule.type) { + case "free_shipping": + if (d.rule.type === rule.type) { + return discount } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } else if ( - shippingDisc.length > 0 && - discount.discount_rule.type === "free_shipping" - ) { - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $pull: { discounts: { _id: shippingDisc[0]._id } }, - $push: { discounts: discount }, + return d + default: + if (!sawNotShipping) { + sawNotShipping = true + if (rule.type !== "free_shipping") { + return discount + } + return d } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } + return null + } + }) - // replace the current discount if there, else add the new one - if (otherDisc.length === 0) { - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $push: { discounts: discount }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } else { - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $pull: { discounts: { _id: otherDisc[0]._id } }, - $push: { discounts: discount }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } + cart.discounts = newDiscounts.filter(Boolean) } + /** + * Removes a discount based on a discount code. + * @param {string} cartId - the id of the cart to remove from + * @param {string} code - the discount code to remove + * @return {Promise} the resulting cart + */ async removeDiscount(cartId, discountCode) { - const cart = await this.retrieve(cartId) - return this.cartModel_.updateOne( - { _id: cart._id }, - { - $pull: { discounts: { code: discountCode } }, + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: ["discounts", "payment_sessions"], + }) + cart.discounts = cart.discounts.filter(d => d.code !== discountCode) + + const cartRepo = manager.getCustomRepository(this.cartRepository_) + + const result = await cartRepo.save(cart) + + if (cart.payment_sessions?.length) { + await this.setPaymentSessions(cartId) } - ) + + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + + return result + }) } /** @@ -807,26 +896,81 @@ class CartService extends BaseService { */ /** - * Retrieves an open payment session from the list of payment sessions - * stored in the cart. If none is an INVALID_DATA error is thrown. - * @param {string} cartId - the id of the cart to retrieve the session from - * @param {string} providerId - the id of the provider the session belongs to - * @return {PaymentMethod} the session + * Updates the currently selected payment session. */ - async retrievePaymentSession(cartId, providerId) { - const cart = await this.retrieve(cartId) - const session = cart.payment_sessions.find( - ({ provider_id }) => provider_id === providerId - ) + async updatePaymentSession(cartId, update) { + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: ["payment_sessions"], + }) - if (!session) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `The provider_id did not match any open payment sessions` - ) - } + if (cart.payment_session) { + await this.paymentProviderService_.updateSessionData( + cart.payment_session, + update + ) + } - return session + const result = await this.retrieve(cart.id) + + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }) + } + + /** + * Authorizes a payment for a cart. + * Will authorize with chosen payment provider. This will return + * a payment object, that we will use to update our cart payment with. + * Additionally, if the payment does not require more or fails, we will + * set the payment on the cart. + * @param {string} cartId - the id of the cart to authorize payment for + * @param {object} context - object containing whatever is relevant for + * authorizing the payment with the payment provider. As an example, + * this could be IP address or similar for fraud handling. + * @return {Promise} the resulting cart + */ + async authorizePayment(cartId, context = {}) { + return this.atomicPhase_(async manager => { + const cartRepository = manager.getCustomRepository(this.cartRepository_) + + const cart = await this.retrieve(cartId, { + select: ["total"], + relations: ["region", "payment_sessions"], + }) + + // If cart total is 0, we don't perform anything payment related + if (cart.total <= 0) { + cart.completed_at = new Date() + return cartRepository.save(cart) + } + + const session = await this.paymentProviderService_ + .withTransaction(manager) + .authorizePayment(cart.payment_session, context) + + const freshCart = await this.retrieve(cart.id, { + select: ["total"], + relations: ["payment_sessions"], + }) + + if (session.status === "authorized") { + const payment = await this.paymentProviderService_ + .withTransaction(manager) + .createPayment(freshCart) + + freshCart.payment = payment + freshCart.completed_at = new Date() + } + + const updated = await cartRepository.save(freshCart) + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, updated) + return updated + }) } /** @@ -835,38 +979,55 @@ class CartService extends BaseService { * @param {PaymentMethod} paymentMethod - the method to be set to the cart * @returns {Promise} result of update operation */ - async setPaymentMethod(cartId, paymentMethod) { - const cart = await this.retrieve(cartId) - const region = await this.regionService_.retrieve(cart.region_id) + async setPaymentSession(cartId, providerId) { + return this.atomicPhase_(async manager => { + const psRepo = manager.getCustomRepository(this.paymentSessionRepository_) - // The region must have the provider id in its providers array - if ( - !( - region.payment_providers.length && - region.payment_providers.includes(paymentMethod.provider_id) - ) - ) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `The payment method is not available in this region` - ) - } - - // At this point we can register the payment method. - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { payment_method: paymentMethod }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + const cart = await this.retrieve(cartId, { + select: [ + "total", + "subtotal", + "tax_total", + "discount_total", + "gift_card_total", + ], + relations: ["region", "region.payment_providers", "payment_sessions"], }) + + // The region must have the provider id in its providers array + if ( + !( + cart.region.payment_providers.length && + cart.region.payment_providers.find(({ id }) => providerId === id) + ) + ) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `The payment method is not available in this region` + ) + } + + await Promise.all( + cart.payment_sessions.map(ps => { + return psRepo.save({ ...ps, is_selected: null }) + }) + ) + + const sess = cart.payment_sessions.find( + ps => ps.provider_id === providerId + ) + + sess.is_selected = true + + await psRepo.save(sess) + + const result = await this.retrieve(cartId) + + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }, "SERIALIZABLE") } /** @@ -878,152 +1039,156 @@ class CartService extends BaseService { * @param {string} cartId - the id of the cart to set payment session for * @returns {Promise} the result of the update operation. */ - async setPaymentSessions(cartId) { - const cart = await this.retrieve(cartId) - const region = await this.regionService_.retrieve(cart.region_id) + async setPaymentSessions(cartOrCartId) { + return this.atomicPhase_(async manager => { + const psRepo = manager.getCustomRepository(this.paymentSessionRepository_) - const total = await this.totalsService_.getTotal(cart) - - if (total === 0) { - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { payment_sessions: [] }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } - - // If there are existing payment sessions ensure that these are up to date - let sessions = [] - if (cart.payment_sessions && cart.payment_sessions.length) { - sessions = await Promise.all( - cart.payment_sessions.map(async pSession => { - if (!region.payment_providers.includes(pSession.provider_id)) { - return null - } - - let data - try { - data = await this.paymentProviderService_.updateSession( - pSession, - cart - ) - } catch (err) { - data = await this.paymentProviderService_.createSession( - pSession.provider_id, - cart - ) - } - - return { - provider_id: pSession.provider_id, - data, - } - }) - ) - } - - // Filter all null sessions - sessions = sessions.filter(s => !!s) - - // For all the payment providers in the region make sure to either skip them - // if they already exist or create them if they don't yet exist. - let newSessions = await Promise.all( - region.payment_providers.map(async pId => { - if (sessions.find(s => s.provider_id === pId)) { - return null - } - - const data = await this.paymentProviderService_.createSession(pId, cart) - return { - provider_id: pId, - data, - } + let cartId = + typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id + const cart = await this.retrieve(cartId, { + select: [ + "gift_card_total", + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "total", + ], + relations: [ + "items", + "discounts", + "gift_cards", + "billing_address", + "shipping_address", + "region", + "region.payment_providers", + "payment_sessions", + "customer", + ], }) - ) - // Filter null sessions - newSessions = newSessions.filter(s => !!s) + const region = cart.region - // Update the payment sessions with the concatenated array of updated and - // newly created payment sessions - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { payment_sessions: sessions.concat(newSessions) }, + // If there are existing payment sessions ensure that these are up to date + let seen = [] + if (cart.payment_sessions && cart.payment_sessions.length) { + for (const session of cart.payment_sessions) { + if ( + cart.total <= 0 || + !region.payment_providers.find( + ({ id }) => id === session.provider_id + ) + ) { + await this.paymentProviderService_ + .withTransaction(manager) + .deleteSession(session) + } else { + seen.push(session.provider_id) + await this.paymentProviderService_ + .withTransaction(manager) + .updateSession(session, cart) + } } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - } - - async deletePaymentSession(cartId, providerId) { - const cart = await this.retrieve(cartId) - if (cart.payment_sessions) { - const session = cart.payment_sessions.find( - s => s.provider_id === providerId - ) - - if (session) { - // Delete the session with the provider - await this.paymentProviderService_.deleteSession(session) - - const selector = { - $pull: { payment_sessions: { provider_id: providerId } }, - } - - if ( - cart.payment_method && - cart.payment_method.provider_id === providerId - ) { - selector["$set"] = { payment_method: null } - } - - return this.cartModel_ - .updateOne({ _id: cart._id }, selector) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) } - } - return cart + if (cart.total > 0) { + // If only one payment session exists, we preselect it + if (region.payment_providers.length === 1 && !cart.payment_session) { + const p = region.payment_providers[0] + const sess = await this.paymentProviderService_ + .withTransaction(manager) + .createSession(p.id, cart) + + sess.is_selected = true + + await psRepo.save(sess) + } else { + for (const provider of region.payment_providers) { + if (!seen.includes(provider.id)) { + await this.paymentProviderService_ + .withTransaction(manager) + .createSession(provider.id, cart) + } + } + } + } + }, "SERIALIZABLE") } - async updatePaymentSession(cartId, providerId, session) { - const cart = await this.retrieve(cartId) - - return this.cartModel_ - .updateOne( - { - _id: cart._id, - "payment_sessions.provider_id": providerId, - }, - { - $set: { "payment_sessions.$": session }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + /** + * Removes a payment session from the cart. + * @param {string} cartId - the id of the cart to remove from + * @param {string} providerId - the id of the provider whoose payment session + * should be removed. + * @returns {Promise} the resulting cart. + */ + async deletePaymentSession(cartId, providerId) { + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: ["payment_sessions"], }) + + const cartRepo = manager.getCustomRepository(this.cartRepository_) + + if (cart.payment_sessions) { + const session = cart.payment_sessions.find( + ({ provider_id }) => provider_id === providerId + ) + + cart.payment_sessions = cart.payment_sessions.filter( + ({ provider_id }) => provider_id !== providerId + ) + + if (session) { + // Delete the session with the provider + await this.paymentProviderService_ + .withTransaction(manager) + .deleteSession(session) + } + } + + await cartRepo.save(cart) + + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, cart) + return cart + }) + } + + /** + * Refreshes a payment session on a cart + * @param {string} cartId - the id of the cart to remove from + * @param {string} providerId - the id of the provider whoose payment session + * should be removed. + * @returns {Promise} the resulting cart. + */ + async refreshPaymentSession(cartId, providerId) { + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: ["payment_sessions"], + }) + + if (cart.payment_sessions) { + const session = cart.payment_sessions.find( + ({ provider_id }) => provider_id === providerId + ) + + if (session) { + // Delete the session with the provider + await this.paymentProviderService_ + .withTransaction(manager) + .refreshSession(session, cart) + } + } + + const result = await this.retrieve(cartId) + + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }) } /** @@ -1038,79 +1203,52 @@ class CartService extends BaseService { * @return {Promise} the result of the update operation */ async addShippingMethod(cartId, optionId, data) { - const cart = await this.retrieve(cartId) - const { shipping_methods } = cart + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + select: ["subtotal"], + relations: [ + "shipping_methods", + "shipping_methods.shipping_option", + "items", + "items.variant", + "payment_sessions", + "items.variant.product", + ], + }) + const { shipping_methods } = cart - const option = await this.shippingOptionService_.validateCartOption( - optionId, - cart - ) + const newMethod = await this.shippingOptionService_ + .withTransaction(manager) + .createShippingMethod(optionId, data, { cart }) - option.data = await this.shippingOptionService_.validateFulfillmentData( - optionId, - data, - cart - ) - - const profile = await this.shippingProfileService_.list({ - shipping_options: option._id, - }) - if (profile.length !== 1) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Shipping Method must belong to a shipping profile" - ) - } - - option.profile_id = profile[0]._id - - // Go through all existing selected shipping methods and update the one - // that has the same profile as the selected shipping method. - let exists = false - const newMethods = shipping_methods.map(sm => { - if (option.profile_id.equals(sm.profile_id)) { - exists = true - return option + const methods = [newMethod] + if (shipping_methods.length) { + for (const sm of shipping_methods) { + if ( + sm.shipping_option.profile_id === + newMethod.shipping_option.profile_id + ) { + await this.shippingOptionService_ + .withTransaction(manager) + .deleteShippingMethod(sm) + } else { + methods.push(sm) + } + } } - return sm + for (const item of cart.items) { + await this.lineItemService_.withTransaction(manager).update(item.id, { + has_shipping: this.validateLineItemShipping_(methods, item), + }) + } + + const result = await this.retrieve(cartId) + await this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result }) - - // If none of the selected methods are for the same profile as the new - // shipping method the exists flag will be false. Therefore we push the new - // method. - if (!exists) { - newMethods.push(option) - } - - const newItems = await Promise.all( - cart.items.map(async item => { - const hasShipping = await this.validateLineItemShipping_( - newMethods, - item - ) - - return { - ...item, - has_shipping: hasShipping, - } - }) - ) - - return this.cartModel_ - .updateOne( - { - _id: cart._id, - }, - { - $set: { shipping_methods: newMethods, items: newItems }, - } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) } /** @@ -1119,97 +1257,152 @@ class CartService extends BaseService { * @param {string} regionId - the id of the region to set the cart to * @return {Promise} the result of the update operation */ - async setRegion(cartId, regionId, countryCode) { - const cart = await this.retrieve(cartId) - const region = await this.regionService_.retrieve(regionId) - - let update = { - region_id: region._id, + async setRegion_(cart, regionId, countryCode) { + if (cart.completed_at) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Cannot change the region of a completed cart" + ) } + // Set the new region for the cart + const region = await this.regionService_.retrieve(regionId, { + relations: ["countries"], + }) + const addrRepo = this.manager_.getCustomRepository(this.addressRepository_) + cart.region = region + cart.region_id = region.id + // If the cart contains items we want to change the unit_price field of each // item to correspond to the price given in the region if (cart.items.length) { - const newItems = await Promise.all( - cart.items.map(async lineItem => { - try { - lineItem.has_shipping = false - lineItem.content = await this.updateContentPrice_( - lineItem.content, - region._id - ) - } catch (err) { - return null - } - return lineItem - }) - ) + cart.items = await Promise.all( + cart.items + .map(async item => { + const availablePrice = await this.productVariantService_ + .getRegionPrice(item.variant_id, regionId) + .catch(() => undefined) - update.items = newItems.filter(i => !!i) + if (availablePrice !== undefined) { + return this.lineItemService_ + .withTransaction(this.transactionManager_) + .update(item.id, { + has_shipping: false, + unit_price: availablePrice, + }) + } else { + await this.lineItemService_ + .withTransaction(this.transactionManager_) + .delete(item.id) + return null + } + }) + .filter(Boolean) + ) + } + + let shippingAddress = {} + if (cart.shipping_address_id) { + shippingAddress = await addrRepo.findOne({ + where: { id: cart.shipping_address_id }, + }) } - let shippingAddress = cart.shipping_address || {} if (countryCode !== undefined) { - if (!region.countries.includes(countryCode)) { + if ( + !region.countries.find( + ({ iso_2 }) => iso_2 === countryCode.toLowerCase() + ) + ) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, `Country not available in region` ) } - shippingAddress.country_code = countryCode - update.shipping_address = shippingAddress + + const updated = { + ...shippingAddress, + country_code: countryCode.toLowerCase(), + } + + await addrRepo.save(updated) } else { + let updated = { ...shippingAddress } // If the country code of a shipping address is set we need to clear it if (!_.isEmpty(shippingAddress) && shippingAddress.country_code) { - shippingAddress.country_code = "" - update.shipping_address = shippingAddress + updated = { + ...updated, + country_code: null, + } } // If there is only one country in the region preset it if (region.countries.length === 1) { - shippingAddress.country_code = region.countries[0] - update.shipping_address = shippingAddress + updated = { + ...updated, + country_code: region.countries[0].iso_2, + } } + + await addrRepo.save(updated) } // Shipping methods are determined by region so the user needs to find a // new shipping method if (cart.shipping_methods && cart.shipping_methods.length) { - update.shipping_methods = [] + const smRepo = this.manager_.getCustomRepository( + this.shippingMethodRepository_ + ) + await smRepo.remove(cart.shipping_methods) } if (cart.discounts && cart.discounts.length) { const newDiscounts = cart.discounts.map(d => { - if (d.regions.includes(regionId)) { + if (d.regions.find(({ id }) => id === regionId)) { return d } }) - update.discounts = newDiscounts.filter(d => !!d) + cart.discounts = newDiscounts.filter(d => !!d) } - // Payment methods are region specific so the user needs to find a - // new payment method - if (!_.isEmpty(cart.payment_method)) { - update.payment_method = undefined - } + cart.gift_cards = [] if (cart.payment_sessions && cart.payment_sessions.length) { - update.payment_sessions = [] + await Promise.all( + cart.payment_sessions.map(ps => + this.paymentProviderService_ + .withTransaction(this.manager_) + .deleteSession(ps) + ) + ) + cart.payment_sessions = [] + cart.payment_session = null } - - return this.cartModel_ - .updateOne({ _id: cart._id }, { $set: update }) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) } + /** + * Deletes a cart from the database. Completed carts cannot be deleted. + * @param {string} cartId - the id of the cart to delete + * @returns {Promise} the deleted cart or undefined if the cart was + * not found. + */ async delete(cartId) { - const cart = await this.retrieve(cartId) - return this.cartModel_.deleteOne({ _id: cart._id }) + return this.atomicPhase_(async manager => { + const cart = await this.retrieve(cartId, { + relations: ["items", "discounts", "payment_sessions"], + }) + + if (cart.completed_at) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Completed carts cannot be deleted" + ) + } + + const cartRepo = manager.getCustomRepository(this.cartRepository_) + return cartRepo.remove(cartId) + }) } /** @@ -1222,26 +1415,31 @@ class CartService extends BaseService { * @return {Promise} resolves to the updated result. */ async setMetadata(cartId, key, value) { - const validatedId = this.validateId_(cartId) + return this.atomicPhase_(async manager => { + const cartRepo = manager.getCustomRepository(this.cartRepository_) - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } + const validatedId = this.validateId_(cartId) + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } - const keyPath = `metadata.${key}` - return this.cartModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const cart = await cartRepo.findOne(validatedId) + + const existing = cart.metadata || {} + cart.metadata = { + ...existing, + [key]: value, + } + + const result = await cartRepo.save(cart) + this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }) } /** @@ -1251,26 +1449,35 @@ class CartService extends BaseService { * @return {Promise} resolves to the updated result. */ async deleteMetadata(cartId, key) { - const validatedId = this.validateId_(cartId) + return this.atomicPhase_(async manager => { + const cartRepo = manager.getCustomRepository(this.cartRepository_) + const validatedId = this.validateId_(cartId) - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } - const keyPath = `metadata.${key}` - return this.cartModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const cart = await cartRepo.findOne(validatedId) + if (!cart) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Cart with id: ${validatedId} was not found` + ) + } + + const updated = cart.metadata || {} + delete updated[key] + cart.metadata = updated + + const result = await cartRepo.save(cart) + this.eventBus_ + .withTransaction(manager) + .emit(CartService.Events.UPDATED, result) + return result + }) } } diff --git a/packages/medusa/src/services/counter.js b/packages/medusa/src/services/counter.js deleted file mode 100644 index db9b8828b6..0000000000 --- a/packages/medusa/src/services/counter.js +++ /dev/null @@ -1,33 +0,0 @@ -import _ from "lodash" -import { Validator, MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" - -/** - * Provides layer to manipulate carts. - * @implements BaseService - */ -class CounterService extends BaseService { - constructor({ counterModel }) { - super() - - /** @private @const {CartModel} */ - this.counterModel_ = counterModel - } - - async createDefaults() { - const orderCounter = await this.counterModel_.findOne({ _id: "orders" }) - if (!orderCounter) { - await this.counterModel_.create({ _id: "orders", next: 1000 }) - } - } - - async getNext(id) { - const counter = await this.counterModel_.updateOne( - { _id: id }, - { $inc: { next: 1 } } - ) - return counter.next - } -} - -export default CounterService diff --git a/packages/medusa/src/services/customer.js b/packages/medusa/src/services/customer.js index e45788cbb5..fac4b74710 100644 --- a/packages/medusa/src/services/customer.js +++ b/packages/medusa/src/services/customer.js @@ -11,34 +11,46 @@ import { BaseService } from "medusa-interfaces" class CustomerService extends BaseService { static Events = { PASSWORD_RESET: "customer.password_reset", + CREATED: "customer.created", + UPDATED: "customer.updated", } - constructor({ customerModel, eventBusService }) { + constructor({ + manager, + customerRepository, + eventBusService, + addressRepository, + }) { super() - /** @private @const {CustomerModel} */ - this.customerModel_ = customerModel + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {CustomerRepository} */ + this.customerRepository_ = customerRepository /** @private @const {EventBus} */ this.eventBus_ = eventBusService + + /** @private @const {AddressRepository} */ + this.addressRepository_ = addressRepository } - /** - * Used to validate customer ids. Throws an error if the cast fails - * @param {string} rawId - the raw customer id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The customerId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new CustomerService({ + manager: transactionManager, + customerRepository: this.customerRepository_, + eventBusService: this.eventBus_, + addressRepository: this.addressRepository_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -58,7 +70,7 @@ class CustomerService extends BaseService { ) } - return value + return value.toLowerCase() } validateBillingAddress_(address) { @@ -94,7 +106,7 @@ class CustomerService extends BaseService { const secret = customer.password_hash const expiry = Math.floor(Date.now() / 1000) + 60 * 15 // 15 minutes ahead - const payload = { customer_id: customer._id, exp: expiry } + const payload = { customer_id: customer.id, exp: expiry } const token = jwt.sign(payload, secret) // Notify subscribers this.eventBus_.emit(CustomerService.Events.PASSWORD_RESET, { @@ -110,8 +122,13 @@ class CustomerService extends BaseService { * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector, offset, limit) { - return this.customerModel_.find(selector, {}, offset, limit) + async list(selector = {}, config = { relations: [], skip: 0, take: 50 }) { + const customerRepo = this.manager_.getCustomRepository( + this.customerRepository_ + ) + + const query = this.buildQuery_(selector, config) + return customerRepo.find(query) } /** @@ -119,7 +136,10 @@ class CustomerService extends BaseService { * @return {Promise} the result of the count operation */ count() { - return this.customerModel_.count() + const customerRepo = this.manager_.getCustomRepository( + this.customerRepository_ + ) + return customerRepo.count({}) } /** @@ -127,13 +147,14 @@ class CustomerService extends BaseService { * @param {string} customerId - the id of the customer to get. * @return {Promise} the customer document. */ - async retrieve(customerId) { + async retrieve(customerId, config = {}) { + const customerRepo = this.manager_.getCustomRepository( + this.customerRepository_ + ) const validatedId = this.validateId_(customerId) - const customer = await this.customerModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const query = this.buildQuery_({ id: validatedId }, config) + + const customer = await customerRepo.findOne(query) if (!customer) { throw new MedusaError( @@ -141,6 +162,7 @@ class CustomerService extends BaseService { `Customer with ${customerId} was not found` ) } + return customer } @@ -149,11 +171,13 @@ class CustomerService extends BaseService { * @param {string} email - the email of the customer to get. * @return {Promise} the customer document. */ - async retrieveByEmail(email) { - this.validateEmail_(email) - const customer = await this.customerModel_.findOne({ email }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieveByEmail(email, config = {}) { + const customerRepo = this.manager_.getCustomRepository( + this.customerRepository_ + ) + + const query = this.buildQuery_({ email: email.toLowerCase() }, config) + const customer = await customerRepo.findOne(query) if (!customer) { throw new MedusaError( @@ -170,10 +194,13 @@ class CustomerService extends BaseService { * @param {string} phone - the phone of the customer to get. * @return {Promise} the customer document. */ - async retrieveByPhone(phone) { - const customer = await this.customerModel_.findOne({ phone }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieveByPhone(phone, config = {}) { + const customerRepo = this.manager_.getCustomRepository( + this.customerRepository_ + ) + + const query = this.buildQuery_({ phone }, config) + const customer = await customerRepo.findOne(query) if (!customer) { throw new MedusaError( @@ -204,138 +231,168 @@ class CustomerService extends BaseService { * @return {Promise} the result of create */ async create(customer) { - const { email, billing_address, password } = customer - this.validateEmail_(email) - - if (billing_address) { - this.validateBillingAddress_(billing_address) - } - - const existing = await this.retrieveByEmail(email).catch(err => undefined) - - if (existing && password && !existing.has_account) { - const hashedPassword = await this.hashPassword_(password) - customer.password_hash = hashedPassword - customer.has_account = true - delete customer.password - - return this.customerModel_.updateOne( - { _id: existing._id }, - { - $set: customer, - } + return this.atomicPhase_(async manager => { + const customerRepository = manager.getCustomRepository( + this.customerRepository_ ) - } else { - if (password) { + + const { email, billing_address, password } = customer + customer.email = this.validateEmail_(email) + + if (billing_address) { + customer.billing_address = this.validateBillingAddress_(billing_address) + } + + const existing = await this.retrieveByEmail(email).catch(err => undefined) + + if (existing && password && !existing.has_account) { const hashedPassword = await this.hashPassword_(password) customer.password_hash = hashedPassword customer.has_account = true delete customer.password - } - return this.customerModel_.create(customer).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } + const toUpdate = { ...existing, ...customer } + const updated = await customerRepository.save(toUpdate) + await this.eventBus_ + .withTransaction(manager) + .emit(CustomerService.Events.UPDATED, updated) + return updated + } else { + if (password) { + const hashedPassword = await this.hashPassword_(password) + customer.password_hash = hashedPassword + customer.has_account = true + delete customer.password + } + + const created = await customerRepository.create(customer) + const result = await customerRepository.save(created) + await this.eventBus_ + .withTransaction(manager) + .emit(CustomerService.Events.CREATED, result) + return result + } + }) } /** - * Updates a customer. Metadata updates and address updates should - * use dedicated methods, e.g. `setMetadata`, etc. The function - * will throw errors if metadata updates and address updates are attempted. + * Updates a customer. * @param {string} variantId - the id of the variant. 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(customerId, update) { - const customer = await this.retrieve(customerId) - - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" + return this.atomicPhase_(async manager => { + const customerRepository = manager.getCustomRepository( + this.customerRepository_ ) - } - if (update.email) { - this.validateEmail_(update.email) - } + const customer = await this.retrieve(customerId) - if (update.billing_address) { - this.validateBillingAddress_(update.billing_address) - } + const { + email, + password_hash, + billing_address, + metadata, + ...rest + } = update - if (update.password) { - const hashedPassword = await this.hashPassword_(update.password) - update.password_hash = hashedPassword - update.has_account = true - delete update.password - } + if (metadata) { + customer.metadata = this.setMetadata_(customer, metadata) + } - return this.customerModel_ - .updateOne( - { _id: customer._id }, - { $set: update }, - { runValidators: true } - ) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } + if (email) { + customer.email = this.validateEmail_(email) + } - async addOrder(customerId, orderId) { - const customer = await this.retrieve(customerId) - return this.customerModel_.updateOne( - { _id: customer._id }, - { $addToSet: { orders: orderId } } - ) - } + if (billing_address) { + customer.billing_address = this.validateBillingAddress_(billing_address) + } - async addAddress(customerId, address) { - const customer = await this.retrieve(customerId) - this.validateBillingAddress_(address) + for (const [key, value] of Object.entries(rest)) { + customer[key] = value + } - let shouldAdd = !customer.shipping_addresses.find( - a => - a.country_code === address.country_code && - a.address_1 === address.address_1 && - a.address_2 === address.address_2 && - a.city === address.city && - a.phone === address.phone && - a.postal_code === address.postal_code && - a.province === address.province && - a.first_name === address.first_name && - a.last_name === address.last_name - ) - - if (shouldAdd) { - return this.customerModel_.updateOne( - { _id: customer._id }, - { $addToSet: { shipping_addresses: address } } - ) - } else { - return customer - } + const updated = await customerRepository.save(customer) + await this.eventBus_ + .withTransaction(manager) + .emit(CustomerService.Events.UPDATED, updated) + return updated + }) } async updateAddress(customerId, addressId, address) { - const customer = await this.retrieve(customerId) - this.validateBillingAddress_(address) + return this.atomicPhase_(async manager => { + const addressRepo = manager.getCustomRepository(this.addressRepository_) - return this.customerModel_.updateOne( - { _id: customer._id, "shipping_addresses._id": addressId }, - { $set: { "shipping_addresses.$": address } } - ) + const toUpdate = await addressRepo.findOne({ + where: { id: addressId, customer_id: customerId }, + }) + + this.validateBillingAddress_(address) + + for (const [key, value] of Object.entries(address)) { + toUpdate[key] = value + } + + const result = addressRepo.save(toUpdate) + return result + }) } async removeAddress(customerId, addressId) { - const customer = await this.retrieve(customerId) + return this.atomicPhase_(async manager => { + const addressRepo = manager.getCustomRepository(this.addressRepository_) - return this.customerModel_.updateOne( - { _id: customer._id }, - { $pull: { shipping_addresses: { _id: addressId } } } - ) + // Should not fail, if user does not exist, since delete is idempotent + const address = await addressRepo.findOne({ + where: { id: addressId, customer_id: customerId }, + }) + + if (!address) return Promise.resolve() + + await addressRepo.softRemove(address) + + return Promise.resolve() + }) + } + + async addAddress(customerId, address) { + return this.atomicPhase_(async manager => { + const addressRepository = manager.getCustomRepository( + this.addressRepository_ + ) + + const customer = await this.retrieve(customerId, { + relations: ["shipping_addresses"], + }) + this.validateBillingAddress_(address) + + let shouldAdd = !customer.shipping_addresses.find( + a => + a.country_code === address.country_code && + a.address_1 === address.address_1 && + a.address_2 === address.address_2 && + a.city === address.city && + a.phone === address.phone && + a.postal_code === address.postal_code && + a.province === address.province && + a.first_name === address.first_name && + a.last_name === address.last_name + ) + + if (shouldAdd) { + const created = await addressRepository.create({ + customer_id: customerId, + ...address, + }) + const result = await addressRepository.save(created) + return result + } else { + return customer + } + }) } /** @@ -345,16 +402,17 @@ class CustomerService extends BaseService { * @return {Promise} the result of the delete operation. */ async delete(customerId) { - let customer - try { - customer = await this.retrieve(customerId) - } catch (error) { - // Delete is idempotent, but we return a promise to allow then-chaining - return Promise.resolve() - } + return this.atomicPhase_(async manager => { + const customerRepo = manager.getCustomRepository(this.customerRepository_) - return this.customerModel_.deleteOne({ _id: customer._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + // Should not fail, if user does not exist, since delete is idempotent + const customer = await customerRepo.findOne({ where: { id: customerId } }) + + if (!customer) return Promise.resolve() + + await customerRepo.softRemove(customer) + + return Promise.resolve() }) } @@ -372,57 +430,6 @@ class CustomerService extends BaseService { const final = await this.runDecorators_(decorated) return final } - - /** - * Dedicated method to set metadata for a customer. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} customerId - the customer to apply metadata to. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(customerId, key, value) { - const validatedId = this.validateId_(customerId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.customerModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for a customer. - * @param {string} customerId - the customer to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(customerId, key) { - const validatedId = this.validateId_(customerId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.customerModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } } export default CustomerService diff --git a/packages/medusa/src/services/discount.js b/packages/medusa/src/services/discount.js index 7c0d266b71..f060c56df4 100644 --- a/packages/medusa/src/services/discount.js +++ b/packages/medusa/src/services/discount.js @@ -9,8 +9,10 @@ import { Validator, MedusaError } from "medusa-core-utils" */ class DiscountService extends BaseService { constructor({ - discountModel, - dynamicDiscountCodeModel, + manager, + discountRepository, + discountRuleRepository, + giftCardRepository, totalsService, productVariantService, productService, @@ -19,18 +21,21 @@ class DiscountService extends BaseService { }) { super() - /** @private @const {DiscountModel} */ - this.discountModel_ = discountModel + /** @private @const {EntityManager} */ + this.manager_ = manager - /** @private @const {DynamicDiscountCodeModel} */ - this.dynamicCodeModel_ = dynamicDiscountCodeModel + /** @private @const {DiscountRepository} */ + this.discountRepository_ = discountRepository + + /** @private @const {DiscountRuleRepository} */ + this.discountRuleRepository_ = discountRuleRepository + + /** @private @const {GiftCardRepository} */ + this.giftCardRepository_ = giftCardRepository /** @private @const {TotalsService} */ this.totalsService_ = totalsService - /** @private @const {ProductVariantService} */ - this.productVariantService_ = productVariantService - /** @private @const {ProductService} */ this.productService_ = productService @@ -41,22 +46,25 @@ class DiscountService extends BaseService { this.eventBus_ = eventBusService } - /** - * Validates discount id - * @param {string} rawId - the raw id to validate - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The discount id could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new DiscountService({ + manager: transactionManager, + discountRepository: this.discountRepository_, + discountRuleRepository: this.discountRuleRepository_, + giftCardRepository: this.giftCardRepository_, + totalsService: this.totalsService_, + productService: this.productService_, + regionService: this.regionService_, + eventBusService: this.eventBus_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -66,15 +74,14 @@ class DiscountService extends BaseService { */ validateDiscountRule_(discountRule) { const schema = Validator.object().keys({ - description: Validator.string(), + description: Validator.string().optional(), type: Validator.string().required(), value: Validator.number() .min(0) .required(), allocation: Validator.string().required(), - valid_for: Validator.array().items(Validator.string()), - user_limit: Validator.number(), - total_limit: Validator.number(), + valid_for: Validator.array().optional(), + user_limit: Validator.number().optional(), }) const { value, error } = schema.validate(discountRule) @@ -95,21 +102,17 @@ class DiscountService extends BaseService { return value } - /** - * Used to normalize discount codes to uppercase. - * @param {string} discountCode - the discount code to normalize - * @return {string} the normalized discount code - */ - normalizeDiscountCode_(discountCode) { - return discountCode.toUpperCase() - } - /** * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector) { - return this.discountModel_.find(selector) + async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { + const discountRepo = this.manager_.getCustomRepository( + this.discountRepository_ + ) + + const query = this.buildQuery_(selector, config) + return discountRepo.find(query) } /** @@ -119,27 +122,45 @@ class DiscountService extends BaseService { * @return {Promise} the result of the create operation */ async create(discount) { - discount.discount_rule = this.validateDiscountRule_(discount.discount_rule) + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) + const ruleRepo = manager.getCustomRepository(this.discountRuleRepository_) - discount.code = this.normalizeDiscountCode_(discount.code) + const validatedRule = this.validateDiscountRule_(discount.rule) - return this.discountModel_.create(discount).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + if (discount.regions) { + discount.regions = await Promise.all( + discount.regions.map(regionId => + this.regionService_.retrieve(regionId) + ) + ) + } + + const discountRule = await ruleRepo.create(validatedRule) + const createdDiscountRule = await ruleRepo.save(discountRule) + + discount.code = discount.code.toUpperCase() + discount.rule = createdDiscountRule + + const created = await discountRepo.create(discount) + const result = await discountRepo.save(created) + return result }) } /** * Gets a discount by id. * @param {string} discountId - id of discount to retrieve - * @return {Promise} the discount document + * @return {Promise} the discount */ - async retrieve(discountId) { + async retrieve(discountId, config = {}) { + const discountRepo = this.manager_.getCustomRepository( + this.discountRepository_ + ) + const validatedId = this.validateId_(discountId) - const discount = await this.discountModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const query = this.buildQuery_({ id: validatedId }, config) + const discount = await discountRepo.findOne(query) if (!discount) { throw new MedusaError( @@ -147,6 +168,7 @@ class DiscountService extends BaseService { `Discount with ${discountId} was not found` ) } + return discount } @@ -155,38 +177,28 @@ class DiscountService extends BaseService { * @param {string} discountCode - discount code of discount to retrieve * @return {Promise} the discount document */ - async retrieveByCode(discountCode) { - discountCode = this.normalizeDiscountCode_(discountCode) - let discount = await this.discountModel_ - .findOne({ code: discountCode }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieveByCode(discountCode, relations = []) { + const discountRepo = this.manager_.getCustomRepository( + this.discountRepository_ + ) + + let discount = await discountRepo.findOne({ + where: { code: discountCode.toUpperCase(), is_dynamic: false }, + relations, + }) if (!discount) { - const dynamicCode = await this.dynamicCodeModel_ - .findOne({ code: discountCode }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + discount = await discountRepo.findOne({ + where: { code: discountCode.toUpperCase(), is_dynamic: true }, + relations, + }) - if (!dynamicCode) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Discount with code ${discountCode} was not found` - ) - } - - discount = await this.retrieve(dynamicCode.discount_id) if (!discount) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Discount with code ${discountCode} was not found` ) } - - discount.code = discountCode - discount.disabled = dynamicCode.disabled } return discount @@ -199,56 +211,33 @@ class DiscountService extends BaseService { * @return {Promise} the result of the update operation */ async update(discountId, update) { - const discount = await this.retrieve(discountId) + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Use setMetadata to update discount metadata" - ) - } + const discount = await this.retrieve(discountId) - if (update.discount_rule) { - update.discount_rule = this.validateDiscountRule_(update.discount_rule) - } + const { rule, metadata, regions, ...rest } = update - return this.discountModel_.updateOne( - { _id: discount._id }, - { $set: update }, - { runValidators: true } - ) - } + if (regions) { + discount.regions = await Promise.all( + regions.map(regionId => this.regionService_.retrieve(regionId)) + ) + } - /** - * Generates a gift card with the specified value which is valid in the - * specified region. - * @param {number} value - the value that the gift card represents - * @param {string} regionId - the id of the region in which the gift card can - * be used - * @return {Discount} the newly created gift card - */ - async generateGiftCard(value, regionId) { - const region = await this.regionService_.retrieve(regionId) + if (metadata) { + discount.metadata = await this.setMetadata_(discount.id, metadata) + } - const code = [ - randomize("A0", 4), - randomize("A0", 4), - randomize("A0", 4), - randomize("A0", 4), - ].join("-") + if (rule) { + discount.rule = this.validateDiscountRule_(rule) + } - const discountRule = this.validateDiscountRule_({ - type: "fixed", - allocation: "total", - value, - }) + for (const [key, value] of Object.entries(rest)) { + discount[key] = value + } - return this.discountModel_.create({ - code, - discount_rule: discountRule, - is_giftcard: true, - regions: [region._id], - original_amount: value, + const updated = await discountRepo.save(discount) + return updated }) } @@ -259,76 +248,123 @@ class DiscountService extends BaseService { * @return {Promise} the newly created dynamic code */ async createDynamicCode(discountId, data) { - const discount = await this.retrieve(discountId) - if (!discount.is_dynamic) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Discount must be set to dynamic" - ) - } + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) - const code = this.normalizeDiscountCode_(data.code) - return this.dynamicCodeModel_.create({ - ...data, - discount_id: discount._id, - code, + const discount = await this.retrieve(discountId) + + if (!discount.is_dynamic) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Discount must be set to dynamic" + ) + } + + if (!data.code) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Discount must have a code" + ) + } + + const toCreate = { + ...data, + rule_id: discount.rule_id, + is_dynamic: true, + is_disabled: false, + code: data.code.toUpperCase(), + parent_discount_id: discount.id, + } + + const created = await discountRepo.create(toCreate) + const result = await discountRepo.save(created) + return result }) } /** - * Creates a dynamic code for a discount id. + * Deletes a dynamic code for a discount id. * @param {string} discountId - the id of the discount to create a code for * @param {string} code - the code to identify the discount by * @return {Promise} the newly created dynamic code */ async deleteDynamicCode(discountId, code) { - const discont = await this.retrieve(discountId) - if (!discount.is_dynamic) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Discount must be set to dynamic" - ) - } + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) + const discount = await discountRepo.findOne({ + where: { parent_discount_id: discountId, code }, + }) - return this.dynamicCodeModel_.deleteOne({ - code, + if (!discount) return Promise.resolve() + + await discountRepo.softRemove(discount) + + return Promise.resolve() }) } /** - * Adds a valid variant to the discount rule valid_for array. + * Adds a valid product to the discount rule valid_for array. * @param {string} discountId - id of discount - * @param {string} variantId - id of variant to add + * @param {string} productId - id of product to add * @return {Promise} the result of the update operation */ - async addValidVariant(discountId, variantId) { - const discount = await this.retrieve(discountId) + async addValidProduct(discountId, productId) { + return this.atomicPhase_(async manager => { + const discountRuleRepo = manager.getCustomRepository( + this.discountRuleRepository_ + ) - const variant = await this.productVariantService_.retrieve(variantId) + const discount = await this.retrieve(discountId, { + relations: ["rule", "rule.valid_for"], + }) - return this.discountModel_.updateOne( - { _id: discount._id }, - { $push: { discount_rule: { valid_for: variant._id } } }, - { runValidators: true } - ) + const { rule } = discount + + const exists = rule.valid_for.find(p => p.id === productId) + // If product is already present, we return early + if (exists) { + return rule + } + + const product = await this.productService_.retrieve(productId) + + rule.valid_for = [...rule.valid_for, product] + + const updated = await discountRuleRepo.save(rule) + return updated + }) } /** - * Removes a valid variant from the discount rule valid_for array + * Removes a product from the discount rule valid_for array * @param {string} discountId - id of discount - * @param {string} variantId - id of variant to add + * @param {string} productId - id of product to add * @return {Promise} the result of the update operation */ - async removeValidVariant(discountId, variantId) { - const discount = await this.retrieve(discountId) + async removeValidProduct(discountId, productId) { + return this.atomicPhase_(async manager => { + const discountRuleRepo = manager.getCustomRepository( + this.discountRuleRepository_ + ) - const variant = await this.productVariantService_.retrieve(variantId) + const discount = await this.retrieve(discountId, { + relations: ["rule", "rule.valid_for"], + }) - return this.discountModel_.updateOne( - { _id: discount._id }, - { $pull: { discount_rule: { valid_for: variant._id } } }, - { runValidators: true } - ) + const { rule } = discount + + const exists = rule.valid_for.find(p => p.id === productId) + // If product is not present, we return early + if (!exists) { + return rule + } + + rule.valid_for = rule.valid_for.filter(p => p.id !== productId) + + const updated = await discountRuleRepo.save(rule) + return updated + }) } /** @@ -338,15 +374,26 @@ class DiscountService extends BaseService { * @return {Promise} the result of the update operation */ async addRegion(discountId, regionId) { - const discount = await this.retrieve(discountId) + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) - const region = await this.regionService_.retrieve(regionId) + const discount = await this.retrieve(discountId, { + relations: ["regions"], + }) - return this.discountModel_.updateOne( - { _id: discount._id }, - { $push: { regions: region._id } }, - { runValidators: true } - ) + const exists = discount.regions.find(r => r.id === regionId) + // If region is already present, we return early + if (exists) { + return discount + } + + const region = await this.regionService_.retrieve(regionId) + + discount.regions = [...discount.regions, region] + + const updated = await discountRepo.save(discount) + return updated + }) } /** @@ -356,15 +403,24 @@ class DiscountService extends BaseService { * @return {Promise} the result of the update operation */ async removeRegion(discountId, regionId) { - const discount = await this.retrieve(discountId) + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) - const region = await this.regionService_.retrieve(regionId) + const discount = await this.retrieve(discountId, { + relations: ["regions"], + }) - return this.discountModel_.updateOne( - { _id: discount._id }, - { $pull: { regions: region._id } }, - { runValidators: true } - ) + const exists = discount.regions.find(r => r.id === regionId) + // If region is not present, we return early + if (!exists) { + return discount + } + + discount.regions = discount.regions.filter(r => r.id !== regionId) + + const updated = await discountRepo.save(discount) + return updated + }) } /** @@ -373,70 +429,19 @@ class DiscountService extends BaseService { * @return {Promise} the result of the delete operation */ async delete(discountId) { - let discount - try { - discount = await this.retrieve(discountId) - } catch (error) { - // Delete is idempotent, but we return a promise to allow then-chaining + return this.atomicPhase_(async manager => { + const discountRepo = manager.getCustomRepository(this.discountRepository_) + + const discount = await discountRepo.findOne({ where: { id: discountId } }) + + if (!discount) return Promise.resolve() + + await discountRepo.softRemove(discount) + return Promise.resolve() - } - - return this.discountModel_.deleteOne({ _id: discount._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) }) } - /** - * Dedicated method to set metadata for a discount. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} discountId - the id to apply metadata to. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(discountId, key, value) { - const validatedId = this.validateId_(discountId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.discountModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for a discount. - * @param {string} discountId - the discount to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(discountId, key) { - const validatedId = this.validateId_(discountId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.discountModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - /** * Decorates a discount. * @param {Discount} discount - the discount to decorate. @@ -444,49 +449,18 @@ class DiscountService extends BaseService { * @param {string[]} expandFields - fields to expand. * @return {Discount} return the decorated discount. */ - async decorate(discount, fields = [], expandFields = []) { - const requiredFields = [ - "_id", - "code", - "regions", - "discount_rule", - "is_dynamic", - "is_giftcard", - "disabled", - "metadata", - ] - const decorated = _.pick(discount, fields.concat(requiredFields)) + async decorate(discountId, fields = [], expandFields = []) { + const requiredFields = ["id", "code", "is_dynamic", "metadata"] - if (expandFields.includes("valid_for")) { - let prods = {} - decorated.discount_rule.valid_for = await Promise.all( - decorated.discount_rule.valid_for.map(async p => { - if (p in prods) { - return prods[p] - } - const next = await this.productService_.retrieve(p) - prods[p] = next - return next - }) - ) - } + fields = fields.concat(requiredFields) - if (expandFields.includes("regions")) { - let regions = {} - decorated.regions = await Promise.all( - decorated.regions.map(async r => { - if (r in regions) { - return regions[r] - } - const next = await this.regionService_.retrieve(r) - regions[r] = next - return next - }) - ) - } + const discount = await this.retrieve(discountId, { + select: fields, + relations: expandFields, + }) - const final = await this.runDecorators_(decorated) - return final + // const final = await this.runDecorators_(decorated) + return discount } } diff --git a/packages/medusa/src/services/document.js b/packages/medusa/src/services/document.js deleted file mode 100644 index 2357c5b46f..0000000000 --- a/packages/medusa/src/services/document.js +++ /dev/null @@ -1,138 +0,0 @@ -import { Validator, MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" - -/** - * Provides layer to manipulate documents. - * @implements BaseService - */ -class DocumentService extends BaseService { - constructor({ documentModel, eventBusService }) { - super() - - /** @private @const {DocumentModel} */ - this.documentModel_ = documentModel - - /** @private @const {EventBus} */ - this.eventBus_ = eventBusService - } - - /** - * Used to validate document ids. Throws an error if the cast fails - * @param {string} rawId - the raw document id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The documentId could not be casted to an ObjectId" - ) - } - - return value - } - - /** - * Creates a document. - * @return {Promise} the newly created document. - */ - async create(doc) { - return this.documentModel_.create(doc) - } - - /** - * Retrieve a document. - * @return {Promise} the document. - */ - async retrieve(id) { - const validatedId = this.validateId_(id) - return this.documentModel_.findOne({ _id: validatedId }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Updates a customer. Metadata updates and address updates should - * use dedicated methods, e.g. `setMetadata`, etc. The function - * will throw errors if metadata updates and address updates are attempted. - * @param {string} variantId - the id of the variant. 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(id, update) { - const doc = await this.retrieve(id) - - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" - ) - } - - return this.documentModel_ - .updateOne({ _id: doc._id }, { $set: update }, { runValidators: true }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Deletes a document - * @param {string} id - the id of the document to delete. - * @return {Promise} the result of the delete operation. - */ - async delete(id) { - let doc - try { - doc = await this.retrieve(id) - } catch (error) { - return Promise.resolve() - } - - return this.documentModel_.deleteOne({ _id: doc._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Decorates a document object. - * @param {Document} doc - the document to decorate. - * @param {string[]} fields - the fields to include. - * @param {string[]} expandFields - fields to expand. - * @return {Document} return the decorated doc. - */ - async decorate(doc, fields, expandFields = []) { - return doc - } - - /** - * Dedicated method to set metadata for a document. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} id - the document id - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(id, key, value) { - const doc = await this.retrieve(id) - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.documentModel_ - .updateOne({ _id: doc._id }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } -} - -export default DocumentService diff --git a/packages/medusa/src/services/event-bus.js b/packages/medusa/src/services/event-bus.js index d9483db6c5..a5db480c43 100644 --- a/packages/medusa/src/services/event-bus.js +++ b/packages/medusa/src/services/event-bus.js @@ -7,16 +7,11 @@ import Redis from "ioredis" * @interface */ class EventBusService { - constructor({ logger, redisClient, redisSubscriber }, config) { - /** @private {logger} */ - this.logger_ = logger - - /** @private {object} */ - this.observers_ = {} - - /** @private {object} to handle cron jobs */ - this.cronHandlers_ = {} - + constructor( + { manager, logger, stagedJobRepository, redisClient, redisSubscriber }, + config, + singleton = true + ) { const opts = { createClient: type => { switch (type) { @@ -30,17 +25,65 @@ class EventBusService { }, } - /** @private {BullQueue} used for cron jobs */ - this.cronQueue_ = new Bull(`cron-jobs:queue`, opts) + this.config_ = config - /** @private {BullQueue} */ - this.queue_ = new Bull(`${this.constructor.name}:queue`, opts) + /** @private {EntityManager} */ + this.manager_ = manager - // Register our worker to handle emit calls - this.queue_.process(this.worker_) + /** @private {logger} */ + this.logger_ = logger - // Register cron worker - this.cronQueue_.process(this.cronWorker_) + this.stagedJobRepository_ = stagedJobRepository + + if (singleton) { + /** @private {object} */ + this.observers_ = {} + + /** @private {BullQueue} */ + this.queue_ = new Bull(`${this.constructor.name}:queue`, opts) + + /** @private {object} to handle cron jobs */ + this.cronHandlers_ = {} + + this.redisClient_ = redisClient + this.redisSubscriber_ = redisSubscriber + + /** @private {BullQueue} used for cron jobs */ + this.cronQueue_ = new Bull(`cron-jobs:queue`, opts) + + // Register our worker to handle emit calls + this.queue_.process(this.worker_) + + // Register cron worker + this.cronQueue_.process(this.cronWorker_) + + if (process.env.NODE_ENV !== "test") { + this.startEnqueuer() + } + } + } + + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new EventBusService( + { + manager: transactionManager, + stagedJobRepository: this.stagedJobRepository_, + logger: this.logger_, + redisClient: this.redisClient_, + redisSubscriber: this.redisSubscriber_, + }, + this.config_, + false + ) + + cloned.transactionManager_ = transactionManager + cloned.queue_ = this.queue_ + + return cloned } /** @@ -61,6 +104,25 @@ class EventBusService { } } + /** + * Adds a function to a list of event subscribers. + * @param {string} event - the event that the subscriber will listen for. + * @param {func} subscriber - the function to be called when a certain event + * happens. Subscribers must return a Promise. + */ + unsubscribe(event, subscriber) { + if (typeof subscriber !== "function") { + throw new Error("Subscriber must be a function") + } + + if (this.observers_[event]) { + const index = this.observers_[event].indexOf(subscriber) + if (index !== -1) { + this.observers_[event].splice(index, 1) + } + } + } + /** * */ @@ -82,16 +144,67 @@ class EventBusService { * @param {?any} data - the data to send to the subscriber. * @return {BullJob} - the job from our queue */ - emit(eventName, data) { - return this.queue_.add( - { - eventName, + async emit(eventName, data) { + if (this.transactionManager_) { + const stagedJobRepository = this.transactionManager_.getCustomRepository( + this.stagedJobRepository_ + ) + + const created = await stagedJobRepository.create({ + event_name: eventName, data, - }, - { - removeOnComplete: true, + }) + + return stagedJobRepository.save(created) + } else { + this.queue_.add({ eventName, data }, { removeOnComplete: true }) + } + } + + async sleep(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) + } + + async startEnqueuer() { + this.enRun_ = true + this.enqueue_ = this.enqueuer_() + } + + async stopEnqueuer() { + this.enRun_ = false + await this.enqueue_ + } + + async enqueuer_() { + while (this.enRun_) { + const listConfig = { + relations: [], + skip: 0, + take: 1000, } - ) + + const sjRepo = this.manager_.getCustomRepository( + this.stagedJobRepository_ + ) + const jobs = await sjRepo.find({}, listConfig) + + await Promise.all( + jobs.map(job => { + this.queue_ + .add( + { eventName: job.event_name, data: job.data }, + { removeOnComplete: true } + ) + .then(async () => { + await sjRepo.remove(job) + }) + }) + ) + + await this.sleep(3000) + } } /** @@ -115,6 +228,7 @@ class EventBusService { this.logger_.warn( `An error occured while processing ${eventName}: ${err}` ) + console.log(err) return err }) }) diff --git a/packages/medusa/src/services/fulfillment-provider.js b/packages/medusa/src/services/fulfillment-provider.js index 0afa0a0551..3a73adb74c 100644 --- a/packages/medusa/src/services/fulfillment-provider.js +++ b/packages/medusa/src/services/fulfillment-provider.js @@ -9,6 +9,23 @@ class FulfillmentProviderService { this.container_ = container } + async registerInstalledProviders(providers) { + const { manager, fulfillmentProviderRepository } = this.container_ + const model = manager.getCustomRepository(fulfillmentProviderRepository) + 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 fpRepo.find({}) + } + async listFulfillmentOptions(providers) { const result = await Promise.all( providers.map(async p => { @@ -36,6 +53,42 @@ class FulfillmentProviderService { ) } } + + 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) + } } export default FulfillmentProviderService diff --git a/packages/medusa/src/services/fulfillment.js b/packages/medusa/src/services/fulfillment.js index 5308e9bdea..0548ff2510 100644 --- a/packages/medusa/src/services/fulfillment.js +++ b/packages/medusa/src/services/fulfillment.js @@ -8,45 +8,73 @@ import { MedusaError } from "medusa-core-utils" */ class FulfillmentService extends BaseService { constructor({ + manager, totalsService, + fulfillmentRepository, shippingProfileService, + lineItemService, fulfillmentProviderService, }) { super() + /** @private @const {EntityManager} */ + this.manager_ = manager + /** @private @const {TotalsService} */ this.totalsService_ = totalsService + /** @private @const {FulfillmentRepository} */ + this.fulfillmentRepository_ = fulfillmentRepository + + /** @private @const {ShippingProfileService} */ this.shippingProfileService_ = shippingProfileService + /** @private @const {LineItemService} */ + this.lineItemService_ = lineItemService + + /** @private @const {FulfillmentProviderService} */ this.fulfillmentProviderService_ = fulfillmentProviderService } - async partitionItems_(shipping_methods, items) { - let updatedMethods = [] + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new FulfillmentService({ + manager: transactionManager, + totalsService: this.totalsService_, + fulfillmentRepository: this.fulfillmentRepository_, + shippingProfileService: this.shippingProfileService_, + lineItemService: this.lineItemService_, + fulfillmentProviderService: this.fulfillmentProviderService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + + partitionItems_(shippingMethods, items) { + let partitioned = [] // partition order items to their dedicated shipping method - await Promise.all( - shipping_methods.map(async method => { - const { profile_id } = method - const profile = await this.shippingProfileService_.retrieve(profile_id) - // for each method find the items in the order, that are associated - // with the profile on the current shipping method - if (shipping_methods.length === 1) { - method.items = items - } else { - method.items = items.filter(({ content }) => { - if (Array.isArray(content)) { - // we require bundles to have same shipping method, therefore: - return profile.products.includes(content[0].product._id) - } else { - return profile.products.includes(`${content.product._id}`) - } - }) - } - updatedMethods.push(method) - }) - ) - return updatedMethods + for (const method of shippingMethods) { + const temp = { shipping_method: method } + + // for each method find the items in the order, that are associated + // with the profile on the current shipping method + if (shippingMethods.length === 1) { + temp.items = items + } else { + const methodProfile = method.shipping_option.profile_id + + temp.items = items.filter(({ variant }) => { + variant.product.profile_id === methodProfile + }) + } + partitioned.push(temp) + } + return partitioned } /** @@ -62,7 +90,7 @@ class FulfillmentService extends BaseService { async getFulfillmentItems_(order, items, transformer) { const toReturn = await Promise.all( items.map(async ({ item_id, quantity }) => { - const item = order.items.find(i => i._id.equals(item_id)) + const item = order.items.find(i => i.id === item_id) return transformer(item, quantity) }) ) @@ -102,55 +130,136 @@ class FulfillmentService extends BaseService { } /** - * Creates fulfillments for an order. - * In a situation where the order has more than one shipping method, - * we need to partition the order items, such that they can be sent - * to their respective fulfillment provider. - * @param {string} orderId - id of order to cancel. - * @return {Promise} result of the update operation. + * Retrieves a fulfillment by its id. + * @param {string} id - the id of the fulfillment to retrieve + * @return {Fulfillment} the fulfillment */ - async createFulfillment(order, itemsToFulfill, metadata = {}) { - const lineItems = await this.getFulfillmentItems_( - order, - itemsToFulfill, - this.validateFulfillmentLineItem_ + async retrieve(id, config = {}) { + const fulfillmentRepository = this.manager_.getCustomRepository( + this.fulfillmentRepository_ ) - const { shipping_methods } = order + const validatedId = this.validateId_(id) + const query = this.buildQuery_({ id: validatedId }, config) - // partition order items to their dedicated shipping method - const fulfillments = await this.partitionItems_(shipping_methods, lineItems) + const fulfillment = await fulfillmentRepository.findOne(query) - return Promise.all( - fulfillments.map(async method => { - const provider = this.fulfillmentProviderService_.retrieveProvider( - method.provider_id - ) - - const data = await provider.createOrder(method.data, method.items, { - ...order, - }) - - return { - provider_id: method.provider_id, - items: method.items, - data, - metadata, - } - }) - ) + if (!fulfillment) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Fulfillment with id: ${id} was not found` + ) + } + return fulfillment } - async createShipment(order, fulfillment, trackingNumbers, metadata) { - return { - ...fulfillment, - tracking_numbers: trackingNumbers, - shipped_at: Date.now(), - metadata: { + /** + * Creates an order fulfillment + * If items needs to be fulfilled by different provider, we make + * sure to partition those items, and create fulfillment for + * those partitions. + * @param {Order} order - order to create fulfillment for + * @param {{ item_id: string, quantity: number}[]} itemsToFulfill - the items in the order to fulfill + * @param {object} metadata - potential metadata to add + * @return {Fulfillment[]} the created fulfillments + */ + async createFulfillment(order, itemsToFulfill, custom = {}) { + return this.atomicPhase_(async manager => { + const fulfillmentRepository = manager.getCustomRepository( + this.fulfillmentRepository_ + ) + + const lineItems = await this.getFulfillmentItems_( + order, + itemsToFulfill, + this.validateFulfillmentLineItem_ + ) + + const { shipping_methods } = order + + // partition order items to their dedicated shipping method + const fulfillments = this.partitionItems_(shipping_methods, lineItems) + + const created = await Promise.all( + fulfillments.map(async ({ shipping_method, items }) => { + const ful = fulfillmentRepository.create({ + ...custom, + provider_id: shipping_method.shipping_option.provider_id, + items: items.map(i => ({ item_id: i.id, quantity: i.quantity })), + data: {}, + }) + + let result = await fulfillmentRepository.save(ful) + result.data = await this.fulfillmentProviderService_.createFulfillment( + shipping_method, + items, + { ...order }, + { ...result } + ) + + return fulfillmentRepository.save(result) + }) + ) + + return created + }) + } + + /** + * Cancels a fulfillment with the fulfillment provider. + * @param {Fulfillment|string} fulfillmentOrId - the fulfillment object or id. + * @return {Promise} the result of the save operation + * + */ + cancelFulfillment(fulfillmentOrId) { + return this.atomicPhase_(async manager => { + let id = fulfillmentOrId + if (typeof fulfillmentOrId === "object") { + id = fulfillmentOrId.id + } + const fulfillment = await this.retrieve(id) + + await this.fulfillmentProviderService_.cancelFulfillment(fulfillment) + + fulfillment.status = "canceled" + + const fulfillmentRepo = manager.getCustomRepository( + this.fulfillmentRepository_ + ) + const result = await fulfillmentRepo.save(fulfillment) + return result + }) + } + + /** + * Creates a shipment by marking a fulfillment as shipped. Adds + * tracking numbers and potentially more metadata. + * @param {Order} fulfillmentId - the fulfillment to ship + * @param {string[]} trackingNumbers - tracking numbers for the shipment + * @param {object} metadata - potential metadata to add + * @return {Fulfillment} the shipped fulfillment + */ + async createShipment(fulfillmentId, trackingNumbers, metadata) { + return this.atomicPhase_(async manager => { + const fulfillmentRepository = manager.getCustomRepository( + this.fulfillmentRepository_ + ) + + const fulfillment = await this.retrieve(fulfillmentId, { + relations: ["items"], + }) + + const now = new Date() + fulfillment.shipped_at = now + fulfillment.tracking_numbers = trackingNumbers + fulfillment.metadata = { ...fulfillment.metadata, ...metadata, - }, - } + } + + const updated = fulfillmentRepository.save(fulfillment) + return updated + }) } } diff --git a/packages/medusa/src/services/gift-card.js b/packages/medusa/src/services/gift-card.js new file mode 100644 index 0000000000..a56d3acf4f --- /dev/null +++ b/packages/medusa/src/services/gift-card.js @@ -0,0 +1,259 @@ +import _ from "lodash" +import randomize from "randomatic" +import { BaseService } from "medusa-interfaces" +import { Validator, MedusaError } from "medusa-core-utils" + +/** + * Provides layer to manipulate gift cards. + * @implements BaseService + */ +class GiftCardService extends BaseService { + static Events = { + CREATED: "created", + } + + constructor({ + manager, + giftCardRepository, + giftCardTransactionRepository, + regionService, + eventBusService, + }) { + super() + + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {GiftCardRepository} */ + this.giftCardRepository_ = giftCardRepository + + /** @private @const {GiftCardRepository} */ + this.giftCardTransactionRepo_ = giftCardTransactionRepository + + /** @private @const {RegionService} */ + this.regionService_ = regionService + + /** @private @const {EventBus} */ + this.eventBus_ = eventBusService + } + + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new GiftCardService({ + manager: transactionManager, + giftCardRepository: this.giftCardRepository_, + giftCardTransactionRepository: this.giftCardTransactionRepo_, + regionService: this.regionService_, + eventBusService: this.eventBus_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + + /** + * Generates a 16 character gift card code + * @return {string} the generated gift card code + */ + generateCode_() { + const code = [ + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + ].join("-") + + return code + } + + /** + * @param {Object} selector - the query object for find + * @return {Promise} the result of the find operation + */ + async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { + const giftCardRepo = this.manager_.getCustomRepository( + this.giftCardRepository_ + ) + + const query = this.buildQuery_(selector, config) + return giftCardRepo.find(query) + } + + async createTransaction(data) { + return this.atomicPhase_(async manager => { + const gctRepo = manager.getCustomRepository(this.giftCardTransactionRepo_) + const created = gctRepo.create(data) + const saved = await gctRepo.save(created) + return saved.id + }) + } + + /** + * Creates a gift card with provided data given that the data is validated. + * @param {GiftCard} giftCard - the gift card data to create + * @return {Promise} the result of the create operation + */ + async create(giftCard) { + return this.atomicPhase_(async manager => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + if (!giftCard.region_id) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Gift card is missing region_id` + ) + } + + if (!giftCard.order_id) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Gift card is missing order_id` + ) + } + + // Will throw if region does not exist + const region = await this.regionService_.retrieve(giftCard.region_id) + + const code = this.generateCode_() + + const toCreate = { + code, + region_id: region.id, + ...giftCard, + } + + const created = await giftCardRepo.create(toCreate) + const result = await giftCardRepo.save(created) + + await this.eventBus_ + .withTransaction(manager) + .emit(GiftCardService.Events.CREATED, { + id: result.id, + }) + + return result + }) + } + + /** + * Gets a gift card by id. + * @param {string} giftCardId - id of gift card to retrieve + * @return {Promise} the gift card + */ + async retrieve(giftCardId, config = {}) { + const giftCardRepo = this.manager_.getCustomRepository( + this.giftCardRepository_ + ) + + const validatedId = this.validateId_(giftCardId) + + const query = { + where: { id: validatedId }, + } + + if (config.select) { + query.select = config.select + } + + if (config.relations) { + query.relations = config.relations + } + + const giftCard = await giftCardRepo.findOne(query) + + if (!giftCard) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Gift card with ${giftCardId} was not found` + ) + } + + return giftCard + } + + async retrieveByCode(code, config = {}) { + const giftCardRepo = this.manager_.getCustomRepository( + this.giftCardRepository_ + ) + + const query = { + where: { code }, + } + + if (config.select) { + query.select = config.select + } + + if (config.relations) { + query.relations = config.relations + } + + const giftCard = await giftCardRepo.findOne(query) + + if (!giftCard) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Gift card with ${code} was not found` + ) + } + + return giftCard + } + + /** + * Updates a giftCard. + * @param {string} giftCardId - giftCard id of giftCard to update + * @param {GiftCard} update - the data to update the giftCard with + * @return {Promise} the result of the update operation + */ + async update(giftCardId, update) { + return this.atomicPhase_(async manager => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + const giftCard = await this.retrieve(giftCardId) + + const { region_id, metadata, ...rest } = update + + if (region_id && region_id !== giftCard.region_id) { + const region = await this.regionService_.retrieve(region_id) + giftCard.region_id = region.id + } + + if (metadata) { + giftCard.metadata = await this.setMetadata_(giftCard.id, metadata) + } + + for (const [key, value] of Object.entries(rest)) { + giftCard[key] = value + } + + const updated = await giftCardRepo.save(giftCard) + return updated + }) + } + + /** + * Deletes a gift card idempotently + * @param {string} giftCardId - id of gift card to delete + * @return {Promise} the result of the delete operation + */ + async delete(giftCardId) { + return this.atomicPhase_(async manager => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + const giftCard = await giftCardRepo.findOne({ where: { id: giftCardId } }) + + if (!giftCard) return Promise.resolve() + + await giftCardRepo.softRemove(giftCard) + + return Promise.resolve() + }) + } +} + +export default GiftCardService diff --git a/packages/medusa/src/services/idempotency-key.js b/packages/medusa/src/services/idempotency-key.js new file mode 100644 index 0000000000..20190aa975 --- /dev/null +++ b/packages/medusa/src/services/idempotency-key.js @@ -0,0 +1,178 @@ +import { BaseService } from "medusa-interfaces" +import { MedusaError } from "medusa-core-utils" +import { v4 } from "uuid" +import IdempotencyKeyModel from "../models/idempotency-key" + +const KEY_LOCKED_TIMEOUT = 1000 + +class IdempotencyKeyService extends BaseService { + constructor({ manager, idempotencyKeyRepository, transactionService }) { + super() + + /** @private @constant {EntityManager} */ + this.manager_ = manager + + /** @private @constant {IdempotencyKeyRepository} */ + this.idempotencyKeyRepository_ = idempotencyKeyRepository + + /** @private @constant {TransactionService} */ + this.transactionService_ = transactionService + } + + /** + * 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 + * @param {object} session - mongoose transaction session + * @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 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/line-item.js b/packages/medusa/src/services/line-item.js index 9ba5e487ca..141b22853f 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -7,9 +7,22 @@ import _ from "lodash" * @implements BaseService */ class LineItemService extends BaseService { - constructor({ productVariantService, productService, regionService }) { + constructor({ + manager, + lineItemRepository, + productVariantService, + productService, + regionService, + cartRepository, + }) { super() + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {LineItemRepository} */ + this.lineItemRepository_ = lineItemRepository + /** @private @const {ProductVariantService} */ this.productVariantService_ = productVariantService @@ -18,155 +31,157 @@ class LineItemService extends BaseService { /** @private @const {RegionService} */ this.regionService_ = regionService + + /** @private @const {CartRepository} */ + this.cartRepository_ = cartRepository + } + + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new LineItemService({ + manager: transactionManager, + lineItemRepository: this.lineItemRepository_, + productVariantService: this.productVariantService_, + productService: this.productService_, + regionService: this.regionService_, + cartRepository: this.cartRepository_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + + async list( + selector, + config = { skip: 0, take: 50, order: { created_at: "DESC" } } + ) { + const liRepo = this.manager_.getCustomRepository(this.lineItemRepository_) + const query = this.buildQuery_(selector, config) + return liRepo.find(query) } /** - * Used to validate line items. - * @param {object} rawLineItem - the raw line item to validate. - * @return {object} the validated id + * Retrieves a line item by its id. + * @param {string} id - the id of the line item to retrieve + * @return {LineItem} the line item */ - validate(rawLineItem) { - const content = Validator.object({ - unit_price: Validator.number().required(), - variant: Validator.object().required(), - product: Validator.object().required(), - quantity: Validator.number() - .integer() - .min(1) - .default(1), - }) - - const lineItemSchema = Validator.object({ - _id: Validator.any().optional(), - title: Validator.string().required(), - description: Validator.string() - .allow("") - .optional(), - thumbnail: Validator.string() - .allow("") - .optional(), - is_giftcard: Validator.bool().optional(), - should_merge: Validator.bool().optional(), - has_shipping: Validator.bool().optional(), - content: Validator.alternatives() - .try(content, Validator.array().items(content)) - .required(), - quantity: Validator.number() - .integer() - .min(1) - .required(), - returned: Validator.bool().optional(), - fulfilled: Validator.bool().optional(), - shipped: Validator.bool().optional(), - fulfilled_quantity: Validator.number() - .integer() - .optional(), - returned_quantity: Validator.number() - .integer() - .optional(), - shipped_quantity: Validator.number() - .integer() - .optional(), - metadata: Validator.object().default({}), - }) - - const { value, error } = lineItemSchema.validate(rawLineItem) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - error.details[0].message - ) - } - - return value - } - - /** - * Contents of a line item - * @typedef {(object | array)} LineItemContent - * @property {number} unit_price - the price of the content - * @property {object} variant - the product variant of the content - * @property {object} product - the product of the content - * @property {number} quantity - the quantity of the content - */ - - /** - * A collection of contents grouped in the same line item - * @typedef {LineItemContent[]} LineItemContentArray - */ - - /** - * Generates a line item. - * @param {string} variantId - id of the line item variant - * @param {*} regionId - id of the cart region - * @param {*} quantity - number of items - * @param {object} metadata - metadata for the line item - */ - async generate(variantId, regionId, quantity, metadata = {}) { - const variant = await this.productVariantService_.retrieve(variantId) - const region = await this.regionService_.retrieve(regionId) - - const products = await this.productService_.list({ variants: variantId }) - // this should never fail, since a variant must have a product associated - // with it to exists, but better safe than sorry - if (!products.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Could not find product for variant with id: ${variantId}` - ) - } - - const product = products[0] - const unit_price = await this.productVariantService_.getRegionPrice( - variant._id, - region._id + async retrieve(id, config = {}) { + const lineItemRepository = this.manager_.getCustomRepository( + this.lineItemRepository_ ) - const line = { - title: product.title, - description: variant.title, - quantity, - should_merge: true, - thumbnail: product.thumbnail, - content: { - unit_price, - variant, - product, - quantity: 1, - }, - metadata: { - ...metadata, - }, - } + const validatedId = this.validateId_(id) + const query = this.buildQuery_({ id: validatedId }, config) - if (product.is_giftcard) { - line.is_giftcard = true - } + const lineItem = await lineItemRepository.findOne(query) - return line - } - - isEqual(line, match) { - if (Array.isArray(line.content)) { - if ( - Array.isArray(match.content) && - match.content.length === line.content.length - ) { - return line.content.every( - (c, index) => - c.variant._id.equals(match[index].variant._id) && - c.quantity === match[index].quantity - ) - } - } else if (!Array.isArray(match.content)) { - return ( - line.content.variant._id.equals(match.content.variant._id) && - line.content.quantity === match.content.quantity && - _.isEqual(line.metadata, match.metadata) + if (!lineItem) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Line item with ${id} was not found` ) } - return false + return lineItem + } + + async generate(variantId, regionId, quantity, metadata = {}) { + const variant = await this.productVariantService_.retrieve(variantId, { + relations: ["product"], + }) + + const region = await this.regionService_.retrieve(regionId) + + const price = await this.productVariantService_.getRegionPrice( + variant.id, + region.id + ) + + const toCreate = { + unit_price: price, + title: variant.product.title, + description: variant.title, + thumbnail: variant.product.thumbnail, + variant_id: variant.id, + quantity: quantity || 1, + allow_discounts: !variant.product.is_giftcard, + is_giftcard: variant.product.is_giftcard, + metadata: metadata || {}, + should_merge: true, + } + + return toCreate + } + + /** + * Create a line item + * @param {LineItem} lineItem - the line item object to create + * @return {LineItem} the created line item + */ + async create(lineItem) { + return this.atomicPhase_(async manager => { + const lineItemRepository = manager.getCustomRepository( + this.lineItemRepository_ + ) + + const created = await lineItemRepository.create(lineItem) + const result = await lineItemRepository.save(created) + return result + }) + } + + /** + * Updates a line item + * @param {string} id - the id of the line item to update + * @param {object} update - the properties to update on line item + * @return {LineItem} the update line item + */ + async update(id, update) { + return this.atomicPhase_(async manager => { + const lineItemRepository = manager.getCustomRepository( + this.lineItemRepository_ + ) + + const lineItem = await this.retrieve(id) + + const { metadata, ...rest } = update + + if (metadata) { + lineItem.metadata = this.setMetadata_(lineItem, metadata) + } + + for (const [key, value] of Object.entries(rest)) { + lineItem[key] = value + } + + const result = await lineItemRepository.save(lineItem) + return result + }) + } + + /** + * Deletes a line item. + * @param {string} id - the id of the line item to delete + * @return {Promise} the result of the delete operation + */ + async delete(id) { + return this.atomicPhase_(async manager => { + const lineItemRepository = manager.getCustomRepository( + this.lineItemRepository_ + ) + + const lineItem = await lineItemRepository.findOne({ where: { id } }) + + if (!lineItem) return Promise.resolve() + + await lineItemRepository.remove(lineItem) + + return Promise.resolve() + }) } } diff --git a/packages/medusa/src/services/oauth.js b/packages/medusa/src/services/oauth.js index a7702594f5..1acee5d6ce 100644 --- a/packages/medusa/src/services/oauth.js +++ b/packages/medusa/src/services/oauth.js @@ -10,37 +10,48 @@ class Oauth extends OauthService { constructor(cradle) { super() + const manager = cradle.manager + + this.manager = manager this.container_ = cradle - this.model_ = cradle.oauthModel + this.oauthRepository_ = cradle.oauthRepository this.eventBus_ = cradle.eventBusService } retrieveByName(appName) { - return this.model_.findOne({ + const repo = this.manager.getCustomRepository(this.oauthRepository_) + return repo.findOne({ application_name: appName, }) } list(selector) { - return this.model_.find(selector) + const repo = this.manager.getCustomRepository(this.oauthRepository_) + return repo.find(selector) } - create(data) { - return this.model_.create({ + 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) } - update(id, update) { - return this.model_.updateOne( - { - _id: id, - }, - update - ) + 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) { @@ -72,7 +83,7 @@ class Oauth extends OauthService { const authData = await service.generateToken(code) - return this.update(app._id, { + return this.update(app.id, { data: authData, }).then(result => { this.eventBus_.emit( @@ -96,7 +107,7 @@ class Oauth extends OauthService { const authData = await service.refreshToken(refreshToken) - return this.update(app._id, { + return this.update(app.id, { data: authData, }).then(result => { this.eventBus_.emit( diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index c779fa544d..e43da23e60 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -1,6 +1,7 @@ import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { Brackets } from "typeorm" class OrderService extends BaseService { static Events = { @@ -13,6 +14,7 @@ class OrderService extends BaseService { ITEMS_RETURNED: "order.items_returned", RETURN_ACTION_REQUIRED: "order.return_action_required", REFUND_CREATED: "order.refund_created", + REFUND_FAILED: "order.refund_failed", SWAP_CREATED: "order.swap_created", SWAP_RECEIVED: "order.swap_received", PLACED: "order.placed", @@ -22,8 +24,8 @@ class OrderService extends BaseService { } constructor({ - orderModel, - counterService, + manager, + orderRepository, customerService, paymentProviderService, shippingOptionService, @@ -36,13 +38,18 @@ class OrderService extends BaseService { regionService, returnService, swapService, - documentService, + cartService, + addressRepository, + giftCardService, eventBusService, }) { super() - /** @private @constant {OrderModel} */ - this.orderModel_ = orderModel + /** @private @constant {EntityManager} */ + this.manager_ = manager + + /** @private @constant {OrderRepository} */ + this.orderRepository_ = orderRepository /** @private @constant {CustomerService} */ this.customerService_ = customerService @@ -74,38 +81,59 @@ class OrderService extends BaseService { /** @private @constant {DiscountService} */ this.discountService_ = discountService + /** @private @constant {DiscountService} */ + this.giftCardService_ = giftCardService + /** @private @constant {EventBus} */ this.eventBus_ = eventBusService - /** @private @constant {DocumentService} */ - this.documentService_ = documentService - - /** @private @constant {CounterService} */ - this.counterService_ = counterService - /** @private @constant {ShippingOptionService} */ this.shippingOptionService_ = shippingOptionService + /** @private @constant {CartService} */ + this.cartService_ = cartService + + /** @private @constant {AddressRepository} */ + this.addressRepository_ = addressRepository + /** @private @constant {SwapService} */ this.swapService_ = swapService } + withTransaction(manager) { + if (!manager) { + return this + } + + const cloned = new OrderService({ + manager, + orderRepository: this.orderRepository_, + eventBusService: this.eventBus_, + paymentProviderService: this.paymentProviderService_, + regionService: this.regionService_, + lineItemService: this.lineItemService_, + shippingOptionService: this.shippingOptionService_, + shippingProfileService: this.shippingProfileService_, + fulfillmentProviderService: this.fulfillmentProviderService_, + discountService: this.discountService_, + totalsService: this.totalsService_, + cartService: this.cartService_, + swapService: this.swapService_, + giftCardService: this.giftCardService_, + }) + + cloned.transactionManager_ = manager + + return cloned + } + /** * Used to validate order ids. Throws an error if the cast fails * @param {string} rawId - the raw order id to validate. * @return {string} the validated id */ validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The order id could not be casted to an ObjectId" - ) - } - - return value + return rawId } /** @@ -148,18 +176,131 @@ class OrderService extends BaseService { * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector, offset, limit) { - return this.orderModel_ - .find(selector, {}, offset, limit) - .sort({ created: -1 }) + async list( + selector, + config = { skip: 0, take: 50, order: { created_at: "DESC" } } + ) { + const orderRepo = this.manager_.getCustomRepository(this.orderRepository_) + const query = this.buildQuery_(selector, config) + + const { select, relations, totalsToSelect } = this.transformQueryForTotals_( + config + ) + + if (select && select.length) { + query.select = select + } + + if (relations && relations.length) { + query.relations = relations + } + + const raw = await orderRepo.find(query) + + return raw.map(r => this.decorateTotals_(r, totalsToSelect)) } - /** - * Return the total number of documents in database - * @return {Promise} the result of the count operation - */ - count() { - return this.orderModel_.count() + async listAndCount( + selector, + config = { skip: 0, take: 50, order: { created_at: "DESC" } } + ) { + const orderRepo = this.manager_.getCustomRepository(this.orderRepository_) + + let q + if ("q" in selector) { + q = selector.q + delete selector.q + } + + const query = this.buildQuery_(selector, config) + + if (q) { + const where = query.where + + delete where.display_id + delete where.email + + query.join = { + alias: "order", + innerJoin: { + shipping_address: "order.shipping_address", + }, + } + + query.where = qb => { + qb.where(where) + + qb.andWhere( + new Brackets(qb => { + qb.where(`shipping_address.first_name ILIKE :q`, { q: `%${q}%` }) + .orWhere(`order.email ILIKE :q`, { q: `%${q}%` }) + .orWhere(`display_id::varchar(255) ILIKE :dId`, { dId: `${q}` }) + }) + ) + } + } + + const { select, relations, totalsToSelect } = this.transformQueryForTotals_( + config + ) + + if (select && select.length) { + query.select = select + } + + if (relations && relations.length) { + query.relations = relations + } + + const [raw, count] = await orderRepo.findAndCount(query) + const orders = raw.map(r => this.decorateTotals_(r, totalsToSelect)) + + return [orders, count] + } + + transformQueryForTotals_(config) { + let { select, relations } = config + + if (!select) { + return { + select, + relations, + totalsToSelect: [], + } + } + + const totalFields = [ + "subtotal", + "tax_total", + "shipping_total", + "discount_total", + "gift_card_total", + "total", + "refunded_total", + "refundable_amount", + "items.refundable", + ] + + const totalsToSelect = select.filter(v => totalFields.includes(v)) + if (totalsToSelect.length > 0) { + const relationSet = new Set(relations) + relationSet.add("items") + relationSet.add("discounts") + relationSet.add("gift_cards") + relationSet.add("gift_card_transactions") + relationSet.add("refunds") + relationSet.add("shipping_methods") + relationSet.add("region") + relations = [...relationSet] + + select = select.filter(v => !totalFields.includes(v)) + } + + return { + relations, + select, + totalsToSelect, + } } /** @@ -167,20 +308,36 @@ class OrderService extends BaseService { * @param {string} orderId - id of order to retrieve * @return {Promise} the order document */ - async retrieve(orderId) { + async retrieve(orderId, config = {}) { + const orderRepo = this.manager_.getCustomRepository(this.orderRepository_) const validatedId = this.validateId_(orderId) - const order = await this.orderModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - if (!order) { + const { select, relations, totalsToSelect } = this.transformQueryForTotals_( + config + ) + + const query = { + where: { id: validatedId }, + } + + if (relations && relations.length > 0) { + query.relations = relations + } + + if (select && select.length > 0) { + query.select = select + } + + const raw = await orderRepo.findOne(query) + + if (!raw) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Order with ${orderId} was not found` ) } + + const order = this.decorateTotals_(raw, totalsToSelect) return order } @@ -189,41 +346,35 @@ class OrderService extends BaseService { * @param {string} cartId - cart id to find order * @return {Promise} the order document */ - async retrieveByCartId(cartId) { - const order = await this.orderModel_ - .findOne({ cart_id: cartId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieveByCartId(cartId, config = {}) { + const orderRepo = this.manager_.getCustomRepository(this.orderRepository_) - if (!order) { + const { select, relations, totalsToSelect } = this.transformQueryForTotals_( + config + ) + + const query = { + where: { cart_id: cartId }, + } + + if (relations && relations.length > 0) { + query.relations = relations + } + + if (select && select.length > 0) { + query.select = select + } + + const raw = await orderRepo.findOne(query) + + if (!raw) { throw new MedusaError( MedusaError.Types.NOT_FOUND, - `Order with cart id ${cartId} was not found` + `Order with cart id: ${cartId} was not found` ) } - return order - } - /** - * Gets an order by metadata key value pair. - * @param {string} key - key of metadata - * @param {string} value - value of metadata - * @return {Promise} the order document - */ - async retrieveByMetadata(key, value) { - const order = await this.orderModel_ - .findOne({ metadata: { [key]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - - if (!order) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Order with metadata ${key}: ${value} was not found` - ) - } + const order = this.decorateTotals_(raw, totalsToSelect) return order } @@ -233,12 +384,7 @@ class OrderService extends BaseService { * @return {Promise} the order document */ async existsByCartId(cartId) { - const order = await this.orderModel_ - .findOne({ metadata: { cart_id: cartId } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - + const order = await this.retrieveByCartId(cartId).catch(_ => undefined) if (!order) { return false } @@ -250,149 +396,153 @@ class OrderService extends BaseService { * @return {Promise} the result of the find operation */ async completeOrder(orderId) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId) - // Run all other registered events - const completeOrderJob = await this.eventBus_.emit( - OrderService.Events.COMPLETED, - order - ) + // Run all other registered events + const completeOrderJob = await this.eventBus_.emit( + OrderService.Events.COMPLETED, + { + id: orderId, + } + ) - await completeOrderJob.finished().catch(error => { - throw error + await completeOrderJob.finished().catch(error => { + throw error + }) + + order.status = "completed" + + const orderRepo = manager.getCustomRepository(this.orderRepository_) + return orderRepo.save(order) }) - - return this.orderModel_.updateOne( - { _id: order._id }, - { - $set: { status: "completed" }, - } - ) } /** * Creates an order from a cart - * @param {object} order - the order to create + * @param {string} cartId - id of the cart to create an order from * @return {Promise} resolves to the creation result. */ - async createFromCart(cart) { - // Create DB session for transaction - const dbSession = await this.orderModel_.startSession() + async createFromCart(cartId) { + return this.atomicPhase_(async manager => { + const cart = await this.cartService_ + .withTransaction(manager) + .retrieve(cartId, { + select: ["subtotal", "total"], + relations: [ + "region", + "payment", + "items", + "discounts", + "gift_cards", + "shipping_methods", + ], + }) - if (cart.items.length === 0) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Cannot create order from empty cart" - ) - } + if (cart.items.length === 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Cannot create order from empty cart" + ) + } - // Initialize DB transaction - return dbSession - .withTransaction(async () => { - // Check if order from cart already exists - // If so, this function throws - const exists = await this.existsByCartId(cart._id) - if (exists) { + const exists = await this.existsByCartId(cart.id) + if (exists) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Order from cart already exists" + ) + } + + const { payment, region, total } = cart + // 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, - "Order from cart already exists" + "Cart does not contain a payment method" ) } - const total = await this.totalsService_.getTotal(cart) + const paymentStatus = await this.paymentProviderService_.getStatus( + payment + ) - let paymentSession = {} - let paymentData = {} - const { payment_method, payment_sessions } = cart - - // 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_method) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Cart does not contain a payment method" - ) - } - - if (!payment_sessions || !payment_sessions.length) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "cart must have payment sessions" - ) - } - - paymentSession = payment_sessions.find( - ps => ps.provider_id === payment_method.provider_id - ) - - // Throw if payment method does not exist - if (!paymentSession) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Cart does not have an authorized payment session" - ) - } - - const paymentProvider = this.paymentProviderService_.retrieveProvider( - paymentSession.provider_id - ) - const paymentStatus = await paymentProvider.getStatus( - paymentSession.data - ) - - // If payment status is not authorized, we throw - if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Payment method is not authorized" - ) - } - - paymentData = await paymentProvider.retrievePayment( - paymentSession.data + // If payment status is not authorized, we throw + if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Payment method is not authorized" ) } + } - const region = await this.regionService_.retrieve(cart.region_id) - - let payment = {} - if (paymentSession.provider_id) { - payment = { - provider_id: paymentSession.provider_id, - data: paymentData, - } - } - - const o = { - display_id: await this.counterService_.getNext("orders"), - payment_method: payment, - discounts: cart.discounts, - shipping_methods: cart.shipping_methods, - items: cart.items, - shipping_address: cart.shipping_address, - billing_address: cart.shipping_address, - region_id: cart.region_id, - email: cart.email, - customer_id: cart.customer_id, - cart_id: cart._id, - tax_rate: region.tax_rate, - currency_code: region.currency_code, - metadata: cart.metadata || {}, - } - - const orderDocument = await this.orderModel_ - .create([o], { - session: dbSession, - }) - .catch(err => console.log(err)) - - // Emit and return - this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0]) - return orderDocument[0] + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const o = await orderRepo.create({ + payment_status: "awaiting", + discounts: cart.discounts, + gift_cards: cart.gift_cards, + payment_status: "awaiting", + shipping_methods: cart.shipping_methods, + shipping_address_id: cart.shipping_address_id, + billing_address_id: cart.billing_address_id, + region_id: cart.region_id, + email: cart.email, + customer_id: cart.customer_id, + cart_id: cart.id, + tax_rate: region.tax_rate, + currency_code: region.currency_code, + metadata: cart.metadata || {}, }) - .then(() => this.orderModel_.findOne({ cart_id: cart._id })) + + const result = await orderRepo.save(o) + + await this.paymentProviderService_ + .withTransaction(manager) + .updatePayment(payment.id, { + order_id: result.id, + }) + + let gcBalance = cart.subtotal + for (const g of cart.gift_cards) { + const newBalance = Math.max(0, g.balance - gcBalance) + const usage = g.balance - newBalance + await this.giftCardService_.withTransaction(manager).update(g.id, { + balance: newBalance, + disabled: newBalance === 0, + }) + + await this.giftCardService_.withTransaction(manager).createTransaction({ + gift_card_id: g.id, + order_id: result.id, + amount: usage, + }) + + gcBalance = gcBalance - usage + } + + for (const method of cart.shipping_methods) { + await this.shippingOptionService_ + .withTransaction(manager) + .updateShippingMethod(method.id, { order_id: result.id }) + } + + for (const item of cart.items) { + await this.lineItemService_ + .withTransaction(manager) + .update(item.id, { order_id: result.id }) + } + + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.PLACED, { + id: result.id, + }) + + return result + }) } /** @@ -408,71 +558,52 @@ class OrderService extends BaseService { * @return {order} the resulting order following the update. */ async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { relations: ["items"] }) + const shipment = await this.fulfillmentService_.retrieve(fulfillmentId) - const shipment = order.fulfillments.find(f => f._id.equals(fulfillmentId)) - if (!shipment) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Could not find fulfillment" - ) - } + if (!shipment || shipment.order_id !== orderId) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Could not find fulfillment" + ) + } - const updated = await this.fulfillmentService_.createShipment( - order, - shipment, - trackingNumbers, - metadata - ) + const shipmentRes = await this.fulfillmentService_ + .withTransaction(manager) + .createShipment(fulfillmentId, trackingNumbers, metadata) - let fulfillmentStatus = "shipped" + order.fulfillment_status = "shipped" + for (const item of order.items) { + const shipped = shipmentRes.items.find(si => si.item_id === item.id) + if (shipped) { + const shippedQty = (item.shipped_quantity || 0) + shipped.quantity + if (shippedQty !== item.quantity) { + order.fulfillment_status = "partially_shipped" + } - const newItems = order.items.map(item => { - const shipped = updated.items.find(fi => item._id.equals(fi._id)) - if (shipped) { - // Find the new fulfilled total - const shippedQuantity = (item.shipped_quantity || 0) + shipped.quantity - - // If the ordered quantity is not the same as the fulfilled quantity - // the order cannot be marked as fulfilled. We instead set it as - // partially_fulfilled. - if (item.quantity !== shippedQuantity) { - fulfillmentStatus = "partially_shipped" - } - - return { - ...item, - shipped: item.quantity === shippedQuantity, - shipped_quantity: shippedQuantity, + await this.lineItemService_.withTransaction(manager).update(item.id, { + shipped_quantity: shippedQty, + }) + } else { + if (item.shipped_quantity !== item.quantity) { + order.fulfillment_status = "partially_shipped" + } } } - if (!item.shipped) { - fulfillmentStatus = "partially_shipped" - } + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const result = await orderRepo.save(order) - return item - }) - - // Add the shipment to the order - return this.orderModel_ - .updateOne( - { _id: orderId, "fulfillments._id": fulfillmentId }, - { - $set: { - "fulfillments.$": updated, - items: newItems, - fulfillment_status: fulfillmentStatus, - }, - } - ) - .then(result => { - this.eventBus_.emit(OrderService.Events.SHIPMENT_CREATED, { - order_id: orderId, - shipment: result.fulfillments.find(f => f._id.equals(fulfillmentId)), + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.SHIPMENT_CREATED, { + id: orderId, + fulfillment_id: shipmentRes.id, }) - return result - }) + + return result + }) } /** @@ -480,17 +611,128 @@ class OrderService extends BaseService { * @param {object} order - the order to create * @return {Promise} resolves to the creation result. */ - async create(order) { - return this.orderModel_ - .create(order) - .then(result => { - // Notify subscribers - this.eventBus_.emit(OrderService.Events.PLACED, result) - return result + async create(data) { + return this.atomicPhase_(async manager => { + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const order = orderRepo.create(data) + const result = await orderRepo.save(order) + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.PLACED, { + id: result.id, + }) + return result + }) + } + + /** + * Updates the order's billing address. + * @param {string} orderId - the id of the order to update + * @param {object} address - the value to set the billing address to + * @return {Promise} the result of the update operation + */ + async updateBillingAddress_(order, address) { + const addrRepo = this.manager_.getCustomRepository(this.addressRepository_) + address.country_code = address.country_code.toLowerCase() + + const region = await this.regionService_.retrieve(order.region_id, { + relations: ["countries"], + }) + + if (!region.countries.find(({ iso_2 }) => address.country_code === iso_2)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Shipping country must be in the order region" + ) + } + + address.country_code = address.country_code.toLowerCase() + + if (order.billing_address_id) { + const addr = await addrRepo.findOne({ + where: { id: order.billing_address_id }, }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + + await addrRepo.save({ ...addr, ...address }) + } else { + const created = await addrRepo.create({ ...address }) + await addrRepo.save(created) + } + } + + /** + * Updates the order's shipping address. + * @param {string} orderId - the id of the order to update + * @param {object} address - the value to set the shipping address to + * @return {Promise} the result of the update operation + */ + async updateShippingAddress_(order, address) { + const addrRepo = this.manager_.getCustomRepository(this.addressRepository_) + address.country_code = address.country_code.toLowerCase() + + const region = await this.regionService_.retrieve(order.region_id, { + relations: ["countries"], + }) + + if (!region.countries.find(({ iso_2 }) => address.country_code === iso_2)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Shipping country must be in the order region" + ) + } + + if (order.shipping_address_id) { + const addr = await addrRepo.findOne({ + where: { id: order.shipping_address_id }, }) + + await addrRepo.save({ ...addr, ...address }) + } else { + const created = await addrRepo.create({ ...address }) + await addrRepo.save(created) + } + } + + async addShippingMethod(orderId, optionId, data, config = {}) { + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { + select: ["subtotal"], + relations: [ + "shipping_methods", + "shipping_methods.shipping_option", + "items", + "items.variant", + "items.variant.product", + ], + }) + const { shipping_methods } = order + + const newMethod = await this.shippingOptionService_ + .withTransaction(manager) + .createShippingMethod(optionId, data, { order, ...config }) + + const methods = [newMethod] + if (shipping_methods.length) { + for (const sm of shipping_methods) { + if ( + sm.shipping_option.profile_id === + newMethod.shipping_option.profile_id + ) { + await this.shippingOptionService_ + .withTransaction(manager) + .deleteShippingMethod(sm) + } else { + methods.push(sm) + } + } + } + + const result = await this.retrieve(orderId) + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.UPDATED, { id: result.id }) + return result + }) } /** @@ -503,71 +745,71 @@ class OrderService extends BaseService { * @return {Promise} resolves to the update result. */ async update(orderId, update) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId) - if ( - (update.shipping_address || - update.billing_address || - update.payment_method || - update.items) && - (order.fulfillment_status !== "not_fulfilled" || - order.payment_status !== "awaiting" || - order.status !== "pending") - ) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Can't update shipping, billing, items and payment method when order is processed" - ) - } + if ( + (update.payment || update.items) && + (order.fulfillment_status !== "not_fulfilled" || + order.payment_status !== "awaiting" || + order.status !== "pending") + ) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't update shipping, billing, items and payment method when order is processed" + ) + } - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" - ) - } + if (update.status || update.fulfillment_status || update.payment_status) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't update order statuses. This will happen automatically. Use metadata in order for additional statuses" + ) + } - if (update.status || update.fulfillment_status || update.payment_status) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Can't update order statuses. This will happen automatically. Use metadata in order for additional statuses" - ) - } + const { + metadata, + items, + billing_address, + shipping_address, + ...rest + } = update - const updateFields = { ...update } + if ("metadata" in update) { + order.metadata = this.setMetadata_(order, update.metadata) + } - if (update.shipping_address) { - updateFields.shipping_address = this.validateAddress_( - update.shipping_address - ) - } + if ("shipping_address" in update) { + await this.updateShippingAddress_(order, update.shipping_address) + } - if (update.billing_address) { - updateFields.billing_address = this.validateAddress_( - update.billing_address - ) - } + if ("billing_address" in update) { + await this.updateBillingAddress_(order, update.billing_address) + } - if (update.items) { - updateFields.items = update.items.map(item => - this.lineItemService_.validate(item) - ) - } + if ("items" in update) { + for (const item of update.items) { + await this.lineItemService_.withTransaction(manager).create({ + ...item, + order_id: orderId, + }) + } + } - return this.orderModel_ - .updateOne( - { _id: order._id }, - { $set: updateFields }, - { runValidators: true } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(OrderService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + for (const [key, value] of Object.entries(rest)) { + order[key] = value + } + + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const result = await orderRepo.save(order) + + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.UPDATED, { + id: orderId, + }) + return result + }) } /** @@ -578,64 +820,46 @@ class OrderService extends BaseService { * @return {Promise} result of the update operation. */ async cancel(orderId) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { + relations: ["fulfillments", "payments"], + }) - if (order.payment_status !== "awaiting") { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Can't cancel an order with a processed payment" - ) - } - - const fulfillments = await Promise.all( - order.fulfillments.map(async fulfillment => { - const { provider_id, data } = fulfillment - const provider = await this.fulfillmentProviderService_.retrieveProvider( - provider_id + if (order.payment_status !== "awaiting") { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't cancel an order with a processed payment" ) - const newData = await provider.cancelFulfillment(data) - return { - ...fulfillment, - is_canceled: true, - data: newData, - } - }) - ) + } - const { provider_id, data } = order.payment_method - const paymentProvider = await this.paymentProviderService_.retrieveProvider( - provider_id - ) - - // Cancel payment with payment provider - const payData = await paymentProvider.cancelPayment(data) - - return this.orderModel_ - .updateOne( - { - _id: orderId, - }, - { - $set: { - status: "canceled", - fulfillment_status: "canceled", - payment_status: "canceled", - fulfillments, - payment_method: { - ...order.payment_method, - data: payData, - }, - }, - } + await Promise.all( + order.fulfillments.map(fulfillment => + this.fulfillmentService_ + .withTransaction(manager) + .cancelFulfillment(fulfillment) + ) ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(OrderService.Events.CANCELED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + + for (const p of order.payments) { + await this.paymentProviderService_ + .withTransaction(manager) + .cancelPayment(p) + } + + order.status = "canceled" + order.fulfillment_status = "canceled" + order.payment_status = "canceled" + + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const result = await orderRepo.save(order) + + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.CANCELED, { + id: order.id, + }) + return result + }) } /** @@ -644,56 +868,51 @@ class OrderService extends BaseService { * @return {Promise} result of the update operation. */ async capturePayment(orderId) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const order = await this.retrieve(orderId, { relations: ["payments"] }) - if (order.payment_status !== "awaiting") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Payment already captured" - ) - } + const payments = [] + for (const p of order.payments) { + if (p.captured_at === null) { + const result = await this.paymentProviderService_ + .withTransaction(manager) + .capturePayment(p) + .catch(err => { + this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.PAYMENT_CAPTURE_FAILED, { + id: orderId, + payment_id: p.id, + error: err, + }) + }) - const updateFields = { payment_status: "captured" } - - const { provider_id, data } = order.payment_method - const paymentProvider = await this.paymentProviderService_.retrieveProvider( - provider_id - ) - - try { - await paymentProvider.capturePayment(data) - } catch (error) { - return this.orderModel_ - .updateOne( - { - _id: orderId, - }, - { - $set: { payment_status: "requires_action" }, + if (result) { + payments.push(result) + } else { + payments.push(p) } - ) - .then(result => { - this.eventBus_.emit( - OrderService.Events.PAYMENT_CAPTURE_FAILED, - result - ) - return result - }) - } - - return this.orderModel_ - .updateOne( - { - _id: orderId, - }, - { - $set: updateFields, + } else { + payments.push(p) } - ) - .then(result => { - this.eventBus_.emit(OrderService.Events.PAYMENT_CAPTURED, result) - return result - }) + } + + order.payments = payments + order.payment_status = payments.every(p => p.captured_at !== null) + ? "captured" + : "requires_action" + + const result = await orderRepo.save(order) + + this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.PAYMENT_CAPTURED, { + id: result.id, + }) + + return result + }) } /** @@ -736,74 +955,83 @@ class OrderService extends BaseService { * @return {Promise} result of the update operation. */ async createFulfillment(orderId, itemsToFulfill, metadata = {}) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { + relations: [ + "region", + "fulfillments", + "shipping_address", + "billing_address", + "shipping_methods", + "shipping_methods.shipping_option", + "items", + "items.variant", + "items.variant.product", + "payments", + ], + }) - const fulfillments = await this.fulfillmentService_.createFulfillment( - order, - itemsToFulfill, - metadata - ) - - let successfullyFulfilled = [] - for (const f of fulfillments) { - successfullyFulfilled = [...successfullyFulfilled, ...f.items] - } - - const updateFields = {} - - // Reflect the fulfillments in the items - updateFields.items = order.items.map(i => { - const ful = successfullyFulfilled.find(f => i._id.equals(f._id)) - if (ful) { - // Find the new fulfilled total - const fulfilledQuantity = i.fulfilled_quantity + ful.quantity - - // If the ordered quantity is not the same as the fulfilled quantity - // the order cannot be marked as fulfilled. We instead set it as - // partially_fulfilled. - if (i.quantity !== fulfilledQuantity) { - updateFields.fulfillment_status = "partially_fulfilled" - } - - // Update the items - return { - ...i, - fulfilled: i.quantity === fulfilledQuantity, - fulfilled_quantity: fulfilledQuantity, - } + if (!order.shipping_methods?.length) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Cannot fulfill an order that lacks shipping methods" + ) } - if (!i.fulfilled) { - updateFields.fulfillment_status = "partially_fulfilled" + const fulfillments = await this.fulfillmentService_ + .withTransaction(manager) + .createFulfillment(order, itemsToFulfill, { + metadata, + order_id: orderId, + }) + let successfullyFulfilled = [] + for (const f of fulfillments) { + successfullyFulfilled = [...successfullyFulfilled, ...f.items] } - return i - }) + order.fulfillment_status = "fulfilled" - updateFields.fulfillment_status = "fulfilled" + // Update all line items to reflect fulfillment + for (const item of order.items) { + const fulfillmentItem = successfullyFulfilled.find( + f => item.id === f.item_id + ) - return this.orderModel_ - .updateOne( - { - _id: orderId, - }, - { - $addToSet: { fulfillments: { $each: fulfillments } }, - $set: updateFields, - } - ) - .then(result => { - for (const fulfillment of fulfillments) { - this.eventBus_.emit(OrderService.Events.FULFILLMENT_CREATED, { - order_id: orderId, - fulfillment, + if (fulfillmentItem) { + const fulfilledQuantity = + (item.fulfilled_quantity || 0) + fulfillmentItem.quantity + + // Update the fulfilled quantity + await this.lineItemService_.withTransaction(manager).update(item.id, { + fulfilled_quantity: fulfilledQuantity, }) + + if (item.quantity !== fulfilledQuantity) { + order.fulfillment_status = "partially_fulfilled" + } + } else { + if (item.quantity !== item.fulfilled_quantity) { + order.fulfillment_status = "partially_fulfilled" + } } - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + } + + const orderRepo = manager.getCustomRepository(this.orderRepository_) + + order.fulfillments = [...order.fulfillments, ...fulfillments] + const result = await orderRepo.save(order) + + for (const fulfillment of fulfillments) { + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.FULFILLMENT_CREATED, { + id: orderId, + fulfillment_id: fulfillment.id, + }) + } + + return result + }) } /** @@ -845,7 +1073,7 @@ class OrderService extends BaseService { ) } - const returnable = item.quantity - item.returned_quantity + const returnable = item.quantity - (item.returned_quantity || 0) if (quantity > returnable) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, @@ -859,22 +1087,6 @@ class OrderService extends BaseService { } } - /** - * Generates documents. - * @param {Array} docs - documents to generate - * @param {Function} transformer - a function to apply to the created document - * before returning. - * @return {Promise>} returns the created documents - */ - createDocuments_(docs, transformer) { - return Promise.all( - docs.map(async d => { - const doc = await this.documentService_.create(d) - return transformer(doc) - }) - ) - } - /** * Creates a return request for an order, with given items, and a shipping * method. If no refundAmount is provided the refund amount is calculated from @@ -889,33 +1101,37 @@ class OrderService extends BaseService { * @returns {Promise} the resulting order. */ async requestReturn(orderId, items, shippingMethod, refundAmount) { - const order = await this.retrieve(orderId) - - const returnRequest = await this.returnService_.requestReturn( - order, - items, - shippingMethod, - refundAmount - ) - - return this.orderModel_ - .updateOne( - { - _id: order._id, - }, - { - $push: { - returns: returnRequest, - }, - } - ) - .then(result => { - this.eventBus_.emit(OrderService.Events.RETURN_REQUESTED, { - order: result, - return: returnRequest, - }) - return result + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { + select: ["refunded_total", "total"], + relations: ["items"], }) + + const returnObj = { + order_id: orderId, + items, + shipping_method: shippingMethod, + refund_amount: refundAmount, + } + + const returnRequest = await this.returnService_ + .withTransaction(manager) + .create(returnObj, order) + + const fulfilledReturn = await this.returnService_ + .withTransaction(manager) + .fulfill(returnRequest.id) + + const result = await this.retrieve(orderId) + + this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.RETURN_REQUESTED, { + id: result.id, + return_id: fulfilledReturn.id, + }) + return result + }) } /** @@ -937,97 +1153,75 @@ class OrderService extends BaseService { refundAmount, allowMismatch = false ) { - const order = await this.retrieve(orderId) - const returnRequest = order.returns.find(r => r._id.equals(returnId)) - if (!returnRequest) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Return request with id ${returnId} was not found` - ) - } - - const updatedReturn = await this.returnService_.receiveReturn( - order, - returnRequest, - items, - refundAmount, - allowMismatch - ) - - if (updatedReturn.status === "requires_action") { - return this.orderModel_ - .updateOne( - { - _id: orderId, - "returns._id": updatedReturn._id, - }, - { - $set: { - "returns.$": updatedReturn, - }, - } - ) - .then(result => { - this.eventBus_.emit(OrderService.Events.RETURN_ACTION_REQUIRED, { - order: result, - return: result.returns.find(r => r._id.equals(returnId)), - }) - return result - }) - } - - let isFullReturn = true - const newItems = order.items.map(i => { - const isReturn = updatedReturn.items.find(r => i._id.equals(r.item_id)) - if (isReturn) { - const returnedQuantity = i.returned_quantity + isReturn.quantity - let returned = i.quantity === returnedQuantity - if (!returned) { - isFullReturn = false - } - return { - ...i, - returned_quantity: returnedQuantity, - returned, - } - } else { - if (!i.returned) { - isFullReturn = false - } - return i - } - }) - - const newReturns = order.returns.map(r => - r._id.equals(returnId) ? updatedReturn : r - ) - - const update = { - $set: { - returns: newReturns, - items: newItems, - fulfillment_status: isFullReturn ? "returned" : "partially_returned", - }, - } - - if (updatedReturn.refund_amount > 0) { - const { provider_id, data } = order.payment_method - const paymentProvider = this.paymentProviderService_.retrieveProvider( - provider_id - ) - await paymentProvider.refundPayment(data, updatedReturn.refund_amount) - update.$push = { - refunds: { - amount: updatedReturn.refund_amount, - }, - } - } - - return this.orderModel_.updateOne({ _id: orderId }, update).then(result => { - this.eventBus_.emit(OrderService.Events.ITEMS_RETURNED, { - order: result, - return: updatedReturn, + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { + relations: ["items", "returns", "payments"], }) + const returnRequest = await this.returnService_.retrieve(returnId) + + if (!returnRequest || returnRequest.order_id !== orderId) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Return request with id ${returnId} was not found` + ) + } + + const updatedReturn = await this.returnService_ + .withTransaction(manager) + .receiveReturn(returnId, items, refundAmount, allowMismatch) + + const orderRepo = manager.getCustomRepository(this.orderRepository_) + if (updatedReturn.status === "requires_action") { + order.fulfillment_status = "requires_action" + const result = await orderRepo.save(order) + this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.RETURN_ACTION_REQUIRED, { + id: result.id, + return_id: updatedReturn.id, + }) + return result + } + + let isFullReturn = true + for (const i of order.items) { + const isReturn = updatedReturn.items.find(r => i.id === r.item_id) + if (isReturn) { + const returnedQuantity = + (i.returned_quantity || 0) + isReturn.quantity + if (i.quantity !== returnedQuantity) { + isFullReturn = false + } + + await this.lineItemService_.withTransaction(manager).update(i.id, { + returned_quantity: returnedQuantity, + }) + } else { + if (!i.returned_quantity !== i.quantity) { + isFullReturn = false + } + } + } + + if (updatedReturn.refund_amount > 0) { + await this.paymentProviderService_ + .withTransaction(manager) + .refundPayment(order.payments, updatedReturn.refund_amount, "return") + } + + if (isFullReturn) { + order.fulfillment_status = "returned" + } else { + order.fulfillment_status = "partially_returned" + } + + const result = await orderRepo.save(order) + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.ITEMS_RETURNED, { + id: order.id, + return_id: updatedReturn.id, + }) return result }) } @@ -1039,188 +1233,92 @@ class OrderService extends BaseService { * @return {Promise} the result of the update operation */ async archive(orderId) { - const order = await this.retrieve(orderId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId) - if (order.status !== ("completed" || "refunded")) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Can't archive an unprocessed order" - ) - } - - return this.orderModel_.updateOne( - { - _id: orderId, - }, - { - $set: { status: "archived" }, + if (order.status !== ("completed" || "refunded")) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't archive an unprocessed order" + ) } - ) + + order.status = "archived" + const orderRepo = manager.getCustomRepository(this.orderRepository_) + const result = await orderRepo.save(order) + return result + }) } /** * Refunds a given amount back to the customer. */ async createRefund(orderId, refundAmount, reason, note) { - const order = await this.retrieve(orderId) - const total = await this.totalsService_.getTotal(order) - const refunded = await this.totalsService_.getRefundedTotal(order) - - if (refundAmount > total - refunded) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Cannot refund more than original order amount" - ) - } - - const { provider_id, data } = order.payment_method - const paymentProvider = this.paymentProviderService_.retrieveProvider( - provider_id - ) - - await paymentProvider.refundPayment(data, refundAmount) - - return this.orderModel_ - .updateOne( - { - _id: orderId, - }, - { - $push: { - refunds: { - amount: refundAmount, - reason, - note, - }, - }, - } - ) - .then(result => { - this.eventBus_.emit(OrderService.Events.REFUND_CREATED, { - order: result, - refund: { - amount: refundAmount, - reason, - note, - }, - }) - return result + return this.atomicPhase_(async manager => { + const order = await this.retrieve(orderId, { + select: ["refundable_amount", "total", "refunded_total"], + relations: ["payments"], }) + + if (refundAmount > order.refundable_amount) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Cannot refund more than the original order amount" + ) + } + + const refund = await this.paymentProviderService_ + .withTransaction(manager) + .refundPayment(order.payments, refundAmount, reason, note) + + const result = await this.retrieve(orderId) + this.eventBus_.emit(OrderService.Events.REFUND_CREATED, { + id: result.id, + refund_id: refund.id, + }) + return result + }) } - /** - * Decorates an order. - * @param {Order} order - the order to decorate. - * @param {string[]} fields - the fields to include. - * @param {string[]} expandFields - fields to expand. - * @return {Order} return the decorated order. - */ - async decorate(order, fields = [], expandFields = []) { - if (fields.length === 0) { - // Default to include all fields - fields = [ - "_id", - "display_id", - "status", - "fulfillment_status", - "payment_status", - "email", - "cart_id", - "billing_address", - "shipping_address", - "items", - "currency_code", - "tax_rate", - "fulfillments", - "returns", - "refunds", - "region_id", - "discounts", - "customer_id", - "payment_method", - "shipping_methods", - "documents", - "created", - "metadata", - "shipping_total", - "discount_total", - "tax_total", - "subtotal", - "total", - "refunded_total", - "refundable_amount", - ] + decorateTotals_(order, totalsFields = []) { + if (totalsFields.includes("shipping_total")) { + order.shipping_total = this.totalsService_.getShippingTotal(order) } - const requiredFields = [ - "_id", - "display_id", - "fulfillment_status", - "payment_status", - "status", - "currency_code", - "region_id", - "metadata", - ] - const o = _.pick(order, fields.concat(requiredFields)) - - if (fields.includes("shipping_total")) { - o.shipping_total = await this.totalsService_.getShippingTotal(order) + if (totalsFields.includes("gift_card_total")) { + order.gift_card_total = this.totalsService_.getGiftCardTotal(order) } - if (fields.includes("discount_total")) { - o.discount_total = await this.totalsService_.getDiscountTotal(order) + if (totalsFields.includes("discount_total")) { + order.discount_total = this.totalsService_.getDiscountTotal(order) } - if (fields.includes("tax_total")) { - o.tax_total = await this.totalsService_.getTaxTotal(order) + if (totalsFields.includes("tax_total")) { + order.tax_total = this.totalsService_.getTaxTotal(order) } - if (fields.includes("subtotal")) { - o.subtotal = await this.totalsService_.getSubtotal(order) + if (totalsFields.includes("subtotal")) { + order.subtotal = this.totalsService_.getSubtotal(order) } - if (fields.includes("total")) { - o.total = await this.totalsService_.getTotal(order) + if (totalsFields.includes("total")) { + order.total = this.totalsService_.getTotal(order) } - if (fields.includes("refunded_total")) { - o.refunded_total = await this.totalsService_.getRefundedTotal(order) + if (totalsFields.includes("refunded_total")) { + order.refunded_total = this.totalsService_.getRefundedTotal(order) } - if (fields.includes("refundable_amount")) { - o.refundable_amount = o.total - o.refunded_total + if (totalsFields.includes("refundable_amount")) { + const total = this.totalsService_.getTotal(order) + const refunded_total = this.totalsService_.getRefundedTotal(order) + order.refundable_amount = total - refunded_total } - o.created = order._id.getTimestamp() - - if (expandFields.includes("swaps")) { - if (order.swaps) { - o.swaps = await Promise.all( - order.swaps.map(sId => { - return this.swapService_.retrieve(sId) - }) - ) - } else { - o.swaps = [] - } - } - - if (expandFields.includes("customer")) { - o.customer = await this.customerService_.retrieve(order.customer_id) - } - - if (expandFields.includes("region")) { - o.region = await this.regionService_.retrieve(order.region_id) - } - - if (fields.includes("items")) { - o.items = order.items.map(i => { - return { + if (totalsFields.includes("items.refundable")) { + order.items = order.items.map(i => ({ + ...i, + refundable: this.totalsService_.getLineItemRefund(order, { ...i, - refundable: this.totalsService_.getLineItemRefund(o, { - ...i, - quantity: i.quantity - i.returned_quantity, - }), - } - }) + quantity: i.quantity - (i.returned_quantity || 0), + }), + })) } - const data = await this.runDecorators_(o) - return data + return order } /** @@ -1232,51 +1330,26 @@ class OrderService extends BaseService { * @param {string} value - value for metadata field. * @return {Promise} resolves to the updated result. */ - async setMetadata(orderId, key, value) { - const validatedId = this.validateId_(orderId) + setMetadata_(order, metadata) { + const existing = order.metadata || {} + const newData = {} + for (const [key, value] of Object.entries(metadata)) { + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) + newData[key] = value } - const keyPath = `metadata.${key}` - return this.orderModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Registers a swap to the order. The swap must belong to the order for it to - * be registered. - * @param {string} id - the id of the order to register the swap to. - * @param {string} swapId - the id of the swap to add to the order. - * @returns {Promise} the resulting order - */ - async registerSwapCreated(id, swapId) { - const order = await this.retrieve(id) - const swap = await this.swapService_.retrieve(swapId) - - if (!order._id.equals(swap.order_id)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Swap must belong to the given order" - ) + const updated = { + ...existing, + ...newData, } - return this.orderModel_ - .updateOne({ _id: order._id }, { $addToSet: { swaps: swapId } }) - .then(result => { - this.eventBus_.emit(OrderService.Events.SWAP_CREATED, { - order: result, - swap_id: swapId, - }) - return result - }) + return updated } /** @@ -1287,48 +1360,47 @@ class OrderService extends BaseService { * @returns {Promise} the resulting order */ async registerSwapReceived(id, swapId) { - const order = await this.retrieve(id) - const swap = await this.swapService_.retrieve(swapId) + return this.atomicPhase_(async manager => { + const order = await this.retrieve(id, { relations: ["items"] }) + const swap = await this.swapService_ + .withTransaction(manager) + .retrieve(swapId, { relations: ["return_order", "return_order.items"] }) - if (!order._id.equals(swap.order_id)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Swap must belong to the given order" - ) - } + if (!swap || swap.order_id !== id) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Swap must belong to the given order" + ) + } - if (swap.return.status !== "received") { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Swap is not received" - ) - } + if (swap.return_order.status !== "received") { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Swap is not received" + ) + } - const newItems = order.items.map(i => { - const isReturn = swap.return_items.find(ri => i._id.equals(ri.item_id)) + for (const i of order.items) { + const isReturn = swap.return_order.items.find(ri => i.id === ri.item_id) - if (isReturn) { - const returnedQuantity = i.returned_quantity + isReturn.quantity - const returned = returnedQuantity === i.quantity - return { - ...i, - returned_quantity: returnedQuantity, - returned, + if (isReturn) { + const returnedQuantity = + (i.returned_quantity || 0) + isReturn.quantity + await this.lineItemService_.withTransaction(manager).update(i.id, { + returned_quantity: returnedQuantity, + }) } } - return i - }) - - return this.orderModel_ - .updateOne({ _id: order._id }, { $set: { items: newItems } }) - .then(result => { - this.eventBus_.emit(OrderService.Events.SWAP_RECEIVED, { - order: result, + const result = await this.retrieve(id) + await this.eventBus_ + .withTransaction(manager) + .emit(OrderService.Events.SWAP_RECEIVED, { + id: result.id, swap_id: swapId, }) - return result - }) + return result + }) } /** diff --git a/packages/medusa/src/services/payment-provider.js b/packages/medusa/src/services/payment-provider.js index 9a65508de1..0d2fee1322 100644 --- a/packages/medusa/src/services/payment-provider.js +++ b/packages/medusa/src/services/payment-provider.js @@ -1,12 +1,112 @@ +import { BaseService } from "medusa-interfaces" import { MedusaError } from "medusa-core-utils" /** * Helps retrive payment providers */ -class PaymentProviderService { +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 + + return cloned + } + + async registerInstalledProviders(providers) { + const { manager, paymentProviderRepository } = this.container_ + const model = manager.getCustomRepository(paymentProviderRepository) + 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 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 = options.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 = options.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 } /** @@ -15,9 +115,63 @@ class PaymentProviderService { * @param {Cart} cart - a cart object used to calculate the amount, etc. from * @return {Promise} the payment session */ - createSession(providerId, cart) { - const provider = this.retrieveProvider(providerId) - return provider.createPayment(cart) + 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 {string} providerId - the id of the provider to refresh payment for + * @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 + }) } /** @@ -28,13 +182,38 @@ class PaymentProviderService { * @return {Promise} the updated payment session */ updateSession(paymentSession, cart) { - const provider = this.retrieveProvider(paymentSession.provider_id) - return provider.updatePayment(paymentSession.data, 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) { - const provider = this.retrieveProvider(paymentSession.provider_id) - return provider.deletePayment(paymentSession.data) + 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) + }) } /** @@ -53,6 +232,195 @@ class PaymentProviderService { ) } } + + 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, + }) + + 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 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 created = refundRepo.create({ + order_id, + amount, + reason, + note, + }) + + 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/product-variant.js b/packages/medusa/src/services/product-variant.js index 0c781fa85b..7f40d86823 100644 --- a/packages/medusa/src/services/product-variant.js +++ b/packages/medusa/src/services/product-variant.js @@ -1,5 +1,6 @@ import _ from "lodash" import { BaseService } from "medusa-interfaces" +import { Brackets, Raw, IsNull } from "typeorm" import { Validator, MedusaError } from "medusa-core-utils" /** @@ -13,35 +14,55 @@ class ProductVariantService extends BaseService { } /** @param { productVariantModel: (ProductVariantModel) } */ - constructor({ productVariantModel, eventBusService, regionService }) { + constructor({ + manager, + productVariantRepository, + productRepository, + eventBusService, + regionService, + moneyAmountRepository, + productOptionValueRepository, + }) { super() + /** @private @const {EntityManager} */ + this.manager_ = manager + /** @private @const {ProductVariantModel} */ - this.productVariantModel_ = productVariantModel + this.productVariantRepository_ = productVariantRepository + + /** @private @const {ProductModel} */ + this.productRepository_ = productRepository /** @private @const {EventBus} */ this.eventBus_ = eventBusService /** @private @const {RegionService} */ this.regionService_ = regionService + + this.moneyAmountRepository_ = moneyAmountRepository + + this.productOptionValueRepository_ = productOptionValueRepository } - /** - * Used to validate product ids. Throws an error if the cast fails - * @param {string} rawId - the raw product id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The variantId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new ProductVariantService({ + manager: transactionManager, + productVariantRepository: this.productVariantRepository_, + productRepository: this.productRepository_, + eventBusService: this.eventBus_, + regionService: this.regionService_, + moneyAmountRepository: this.moneyAmountRepository_, + productOptionValueRepository: this.productOptionValueRepository_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -49,100 +70,237 @@ class ProductVariantService extends BaseService { * @param {string} variantId - the id of the product to get. * @return {Promise} the product document. */ - async retrieve(variantId) { + async retrieve(variantId, config = {}) { + const variantRepo = this.manager_.getCustomRepository( + this.productVariantRepository_ + ) const validatedId = this.validateId_(variantId) - const variant = await this.productVariantModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const query = this.buildQuery_({ id: validatedId }, config) + const variant = await variantRepo.findOne(query) if (!variant) { throw new MedusaError( MedusaError.Types.NOT_FOUND, - `Variant with ${variantId} was not found` + `Variant with id: ${variantId} was not found` ) } + return variant } - // TODO: Validate productVariant /** - * Creates an unpublished product variant. + * Gets a product variant by id. + * @param {string} variantId - the id of the product to get. + * @return {Promise} the product document. + */ + async retrieveBySKU(sku, config = {}) { + const variantRepo = this.manager_.getCustomRepository( + this.productVariantRepository_ + ) + const query = this.buildQuery_({ sku }, config) + const variant = await variantRepo.findOne(query) + + if (!variant) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Variant with sku: ${sku} was not found` + ) + } + + return variant + } + + /** + * Creates an unpublished product variant. Will validate against parent product + * to ensure that the variant can in fact be created. + * @param {string} productOrProductId - the product the variant will be added to * @param {object} variant - the variant to create * @return {Promise} resolves to the creation result. */ - async createDraft(productVariant) { - return this.productVariantModel_ - .create({ - ...productVariant, - published: false, + async create(productOrProductId, variant) { + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) + const variantRepo = manager.getCustomRepository( + this.productVariantRepository_ + ) + + const { prices, ...rest } = variant + + let product = productOrProductId + + if (typeof product === `string`) { + product = await productRepo.findOne({ + where: { id: productOrProductId }, + relations: ["variants", "variants.options", "options"], + }) + } else if (!product.id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product id missing` + ) + } + + if (product.options.length !== variant.options.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product options length does not match variant options length. Product has ${product.options.length} and variant has ${variant.options.length}.` + ) + } + + product.options.forEach(option => { + if (!variant.options.find(vo => option.id === vo.option_id)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Variant options do not contain value for ${option.title}` + ) + } }) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.CREATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + + let variantExists = undefined + variantExists = product.variants.find(v => { + return v.options.every(option => { + const variantOption = variant.options.find( + o => option.option_id === o.option_id + ) + + return option.value === variantOption.value + }) }) + + if (variantExists) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Variant with title ${variantExists.title} with provided options already exists` + ) + } + + const toCreate = { + ...rest, + product_id: product.id, + } + + const productVariant = await variantRepo.create(toCreate) + + const result = await variantRepo.save(productVariant) + + if (prices) { + for (const price of prices) { + if (price.region_id) { + await this.setRegionPrice( + result.id, + price.region_id, + price.amount, + price.sale_amount || undefined + ) + } else { + await this.setCurrencyPrice(result.id, price) + } + } + } + + await this.eventBus_ + .withTransaction(manager) + .emit(ProductVariantService.Events.CREATED, { + id: result.id, + }) + + return result + }) } /** - * Creates an publishes variant. - * @param {string} variantId - ID of the variant to publish. - * @return {Promise} resolves to the creation result. + * Publishes an existing variant. + * @param {string} variantId - id of the variant to publish. + * @return {Promise} */ async publish(variantId) { - return this.productVariantModel_ - .updateOne({ _id: variantId }, { $set: { published: true } }) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + return this.atomicPhase_(async manager => { + const variantRepo = manager.getCustomRepository( + this.productVariantRepository_ + ) + + const variant = await this.retrieve(variantId) + + variant.published = true + + const result = await variantRepo.save(variant) + + await this.eventBus_ + .withTransaction(manager) + .emit(ProductVariantService.Events.UPDATED, { + id: result.id, + }) + + return result + }) } /** - * Updates a variant. Metadata updates and price updates should - * use dedicated methods, e.g. `setMetadata`, etc. The function - * will throw errors if metadata updates and price updates are attempted. - * @param {string} variantId - the id of the variant. Must be a string that - * can be casted to an ObjectId + * Updates a variant. + * Price updates should use dedicated methods. + * The function will throw, if price updates are attempted. + * @param {string | ProductVariant} variant - the id of the variant. 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(variantId, update) { - const validatedId = this.validateId_(variantId) - - if (update.prices) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setCurrencyPrices, setRegionPrices method to update prices field" + async update(variantOrVariantId, update) { + return this.atomicPhase_(async manager => { + const variantRepo = manager.getCustomRepository( + this.productVariantRepository_ ) - } + let variant = variantOrVariantId - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" - ) - } + if (typeof variant === `string`) { + variant = await this.retrieve(variantOrVariantId) + } else if (!variant.id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Variant id missing` + ) + } - return this.productVariantModel_ - .updateOne( - { _id: validatedId }, - { $set: update }, - { runValidators: true } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const { prices, options, metadata, ...rest } = update + + if (prices) { + for (const price of prices) { + if (price.region_id) { + await this.setRegionPrice( + variant.id, + price.region_id, + price.amount, + price.sale_amount || undefined + ) + } else { + await this.setCurrencyPrice(variant.id, price) + } + } + } + + if (options) { + for (const option of options) { + await this.updateOptionValue( + variant.id, + option.option_id, + option.value + ) + } + } + + if (metadata) { + variant.metadata = this.setMetadata_(variant, metadata) + } + + for (const [key, value] of Object.entries(rest)) { + variant[key] = value + } + + const result = await variantRepo.save(variant) + await this.eventBus_ + .withTransaction(manager) + .emit(ProductVariantService.Events.UPDATED, result) + return result + }) } /** @@ -153,80 +311,35 @@ class ProductVariantService extends BaseService { * @param {number} saleAmount - the sale amount to set the price to * @return {Promise} the result of the update operation */ - async setCurrencyPrice(variantId, currencyCode, amount, saleAmount) { - const variant = await this.retrieve(variantId) + async setCurrencyPrice(variantId, price) { + return this.atomicPhase_(async manager => { + const moneyAmountRepo = manager.getCustomRepository( + this.moneyAmountRepository_ + ) - // If prices already exist we need to update all prices with the same - // currency - if (variant.prices.length) { - let foundDefault = false - const newPrices = variant.prices.map(moneyAmount => { - if (moneyAmount.currency_code === currencyCode) { - moneyAmount.amount = amount - moneyAmount.sale_amount = saleAmount - - if (!moneyAmount.region_id) { - foundDefault = true - } - } - - return moneyAmount + let moneyAmount + moneyAmount = await moneyAmountRepo.findOne({ + where: { + currency_code: price.currency_code.toLowerCase(), + variant_id: variantId, + region_id: IsNull(), + }, }) - // If there is no price entries for the currency we are updating we need - // to push it - if (!foundDefault) { - newPrices.push({ - currency_code: currencyCode, - sale_amount: saleAmount, - amount, + if (!moneyAmount) { + moneyAmount = await moneyAmountRepo.create({ + ...price, + currency_code: price.currency_code.toLowerCase(), + variant_id: variantId, }) + } else { + moneyAmount.amount = price.amount + moneyAmount.sale_amount = price.sale_amount } - return this.productVariantModel_ - .updateOne( - { - _id: variant._id, - }, - { - $set: { - prices: newPrices, - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - return this.productVariantModel_ - .updateOne( - { - _id: variant._id, - }, - { - $set: { - prices: [ - { - currency_code: currencyCode, - sale_amount: saleAmount, - amount, - }, - ], - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const result = await moneyAmountRepo.save(moneyAmount) + return result + }) } /** @@ -238,43 +351,43 @@ class ProductVariantService extends BaseService { * @return {number} the price specific to the region */ async getRegionPrice(variantId, regionId) { - const variant = await this.retrieve(variantId) - const region = await this.regionService_.retrieve(regionId) + return this.atomicPhase_(async manager => { + const moneyAmountRepo = manager.getCustomRepository( + this.moneyAmountRepository_ + ) - let price - variant.prices.forEach( - ({ region_id, amount, sale_amount, currency_code }) => { - if (!price && !region_id && currency_code === region.currency_code) { - // If we haven't yet found a price and the current money amount is - // the default money amount for the currency of the region we have found - // a possible price match - if (sale_amount) { - price = sale_amount - } else { - price = amount - } - } else if (region_id === region._id) { - // If the region matches directly with the money amount this is the best - // price - if (sale_amount) { - price = sale_amount - } else { - price = amount - } - } + const region = await this.regionService_ + .withTransaction(manager) + .retrieve(regionId) + + // Find region price based on region id + let moneyAmount = await moneyAmountRepo.findOne({ + where: { region_id: regionId, variant_id: variantId }, + }) + + // If no price could be find based on region id, we try to fetch + // based on the region currency code + if (!moneyAmount) { + moneyAmount = await moneyAmountRepo.findOne({ + where: { variant_id: variantId, currency_code: region.currency_code }, + }) } - ) - // Return the price if we found a suitable match - if (typeof price !== "undefined") { - return price - } + // Still, if no price is found, we throw + if (!moneyAmount) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `A price for region: ${region.name} could not be found` + ) + } - // If we got this far no price could be found for the region - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `A price for region: ${region.name} could not be found` - ) + // Always return sale price, if present + if (moneyAmount.sale_amount) { + return moneyAmount.sale_amount + } else { + return moneyAmount.amount + } + }) } /** @@ -282,87 +395,36 @@ class ProductVariantService extends BaseService { * @param {string} variantId - the id of the variant to update * @param {string} regionId - the id of the region to set price for * @param {number} amount - the amount to set the price to - * @param {number} saleAmount - the amount to set the price to + * @param {number} saleAmount - the sale amount to set the price to * @return {Promise} the result of the update operation */ - async setRegionPrice(variantId, regionId, amount, saleAmount) { - const variant = await this.retrieve(variantId) - const region = await this.regionService_.retrieve(regionId) + async setRegionPrice(variantId, price) { + return this.atomicPhase_(async manager => { + const moneyAmountRepo = manager.getCustomRepository( + this.moneyAmountRepository_ + ) - // If prices already exist we need to update all prices with the same currency - if (variant.prices.length) { - let foundRegion = false - const newPrices = variant.prices.map(moneyAmount => { - if (moneyAmount.region_id === region._id) { - moneyAmount.amount = amount - moneyAmount.sale_amount = amount - foundRegion = true - } - - return moneyAmount + let moneyAmount + moneyAmount = await moneyAmountRepo.findOne({ + where: { + variant_id: variantId, + region_id: price.region_id, + }, }) - // If the region doesn't exist in the prices we need to push it - if (!foundRegion) { - newPrices.push({ - region_id: region._id, - currency_code: region.currency_code, - sale_amount: saleAmount, - amount, + if (!moneyAmount) { + moneyAmount = await moneyAmountRepo.create({ + ...price, + variant_id: variantId, }) + } else { + moneyAmount.amount = price.amount + moneyAmount.sale_amount = price.sale_amount } - return this.productVariantModel_ - .updateOne( - { - _id: variant._id, - }, - { - $set: { - prices: newPrices, - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - // Set the price both for default currency price and for the region - return this.productVariantModel_ - .updateOne( - { - _id: variant._id, - }, - { - $set: { - prices: [ - { - region_id: region._id, - currency_code: region.currency_code, - sale_amount: saleAmount, - amount, - }, - { - currency_code: region.currency_code, - sale_amount: saleAmount, - amount, - }, - ], - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const result = await moneyAmountRepo.save(moneyAmount) + return result + }) } /** @@ -374,25 +436,27 @@ class ProductVariantService extends BaseService { * @return {Promise} the result of the update operation. */ async updateOptionValue(variantId, optionId, optionValue) { - if (typeof optionValue !== "string" && typeof optionValue !== "number") { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Option value is not of type string or number` + return this.atomicPhase_(async manager => { + const productOptionValueRepo = manager.getCustomRepository( + this.productOptionValueRepository_ ) - } - return this.productVariantModel_ - .updateOne( - { _id: variantId, "options.option_id": optionId }, - { $set: { "options.$.value": `${optionValue}` } } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + const productOptionValue = await productOptionValueRepo.findOne({ + where: { variant_id: variantId, option_id: optionId }, }) + + if (!productOptionValue) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Product option value not found` + ) + } + + productOptionValue.value = optionValue + + const result = await productOptionValueRepo.save(productOptionValue) + return result + }) } /** @@ -407,52 +471,48 @@ class ProductVariantService extends BaseService { * @return {Promise} the result of the update operation. */ async addOptionValue(variantId, optionId, optionValue) { - const variant = await this.retrieve(variantId) - - if (typeof optionValue !== "string" && typeof optionValue !== "number") { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Option value is not of type string or number` + return this.atomicPhase_(async manager => { + const productOptionValueRepo = manager.getCustomRepository( + this.productOptionValueRepository_ ) - } - return this.productVariantModel_ - .updateOne( - { _id: variant._id }, - { $push: { options: { option_id: optionId, value: `${optionValue}` } } } - ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + const productOptionValue = await productOptionValueRepo.create({ + variant_id: variantId, + option_id: optionId, + value: optionValue, }) + + const result = await productOptionValueRepo.save(productOptionValue) + return result + }) } /** * Deletes option value from given variant. - * Fails when product with variant does not exists or - * if that product has an option with the given - * option id. - * This method should only be used from the product service. + * Will never fail due to delete being idempotent. * @param {string} variantId - the variant to decorate. * @param {string} optionId - the option from product. - * @return {Promise} the result of the update operation. + * @return {Promise} empty promise */ async deleteOptionValue(variantId, optionId) { - return this.productVariantModel_ - .updateOne( - { _id: variantId }, - { $pull: { options: { option_id: optionId } } } + return this.atomicPhase_(async manager => { + const productOptionValueRepo = manager.getCustomRepository( + this.productOptionValueRepository_ ) - .then(result => { - this.eventBus_.emit(ProductVariantService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + + const productOptionValue = await productOptionValueRepo.findOne({ + where: { + variant_id: variantId, + option_id: optionId, + }, }) + + if (!productOptionValue) return Promise.resolve() + + await productOptionValueRepo.softRemove(productOptionValue) + + return Promise.resolve() + }) } /** @@ -476,95 +536,96 @@ class ProductVariantService extends BaseService { * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - async list(selector) { - return this.productVariantModel_.find(selector) - } + async list(selector = {}, config = { relations: [], skip: 0, take: 20 }) { + const productVariantRepo = this.manager_.getCustomRepository( + this.productVariantRepository_ + ) - /** - * Deletes a variant from given variant id. - * @param {string} variantId - the id of the variant to delete. Must be - * castable as an ObjectId - * @return {Promise} the result of the delete operation. - */ - async delete(variantId) { - let variant - try { - variant = await this.retrieve(variantId) - } catch (error) { - // Delete is idempotent, but we return a promise to allow then-chaining - return Promise.resolve() + let q + if ("q" in selector) { + q = selector.q + delete selector.q } - return this.productVariantModel_ - .deleteOne({ _id: variant._id }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const query = this.buildQuery_(selector, config) + + if (q) { + const where = query.where + + delete where.sku + delete where.title + + query.join = { + alias: "variant", + innerJoin: { + product: "variant.product", + }, + } + + query.where = qb => { + qb.where(where).andWhere( + new Brackets(qb => { + qb.where([ + { sku: Raw(a => `${a} ILIKE :q`, { q: `%${q}%` }) }, + { title: Raw(a => `${a} ILIKE :q`, { q: `%${q}%` }) }, + ]).orWhere(`product.title ILIKE :q`, { q: `%${q}%` }) + }) + ) + } + } + + return productVariantRepo.find(query) } /** - * Decorates a variant with variant variants. - * @param {ProductVariant} variant - the variant to decorate. - * @param {string[]} fields - the fields to include. - * @param {string[]} expandFields - fields to expand. - * @return {ProductVariant} return the decorated variant. + * Deletes variant. + * Will never fail due to delete being idempotent. + * @param {string} variantId - the id of the variant to delete. Must be + * castable as an ObjectId + * @return {Promise} empty promise */ - async decorate(variant, fields, expandFields = []) { - const requiredFields = ["_id", "metadata"] - const decorated = _.pick(variant, fields.concat(requiredFields)) - const final = await this.runDecorators_(decorated) - return final + async delete(variantId) { + return this.atomicPhase_(async manager => { + const variantRepo = manager.getCustomRepository( + this.productVariantRepository_ + ) + + const variant = await variantRepo.findOne({ where: { id: variantId } }) + + if (!variant) return Promise.resolve() + + await variantRepo.softRemove(variant) + + return Promise.resolve() + }) } /** * Dedicated method to set metadata for a variant. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} variantId - the variant to decorate. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. + * @param {string} variant - the variant to set metadata for. + * @param {Object} metadata - the metadata to set + * @return {Object} updated metadata object */ - async setMetadata(variantId, key, value) { - const validatedId = this.validateId_(variantId) + setMetadata_(variant, metadata) { + const existing = variant.metadata || {} + const newData = {} + for (const [key, value] of Object.entries(metadata)) { + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) + newData[key] = value } - const keyPath = `metadata.${key}` - return this.productVariantModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for a product variant. - * @param {string} variantId - the product variant to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(variantId, key) { - const validatedId = this.validateId_(variantId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) + const updated = { + ...existing, + ...newData, } - const keyPath = `metadata.${key}` - return this.productVariantModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + return updated } } diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index b1b988a64e..5f2aa41b72 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -1,7 +1,7 @@ -import mongoose from "mongoose" import _ from "lodash" -import { Validator, MedusaError, compareObjectsByProp } from "medusa-core-utils" +import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { Brackets } from "typeorm" /** * Provides layer to manipulate products. @@ -13,12 +13,27 @@ class ProductService extends BaseService { CREATED: "product.created", } - /** @param { productModel: (ProductModel) } */ - constructor({ productModel, eventBusService, productVariantService }) { + constructor({ + manager, + productRepository, + productVariantRepository, + productOptionRepository, + eventBusService, + productVariantService, + }) { super() - /** @private @const {ProductModel} */ - this.productModel_ = productModel + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {ProductOption} */ + this.productOptionRepository_ = productOptionRepository + + /** @private @const {Product} */ + this.productRepository_ = productRepository + + /** @private @const {ProductVariant} */ + this.productVariantRepository_ = productVariantRepository /** @private @const {EventBus} */ this.eventBus_ = eventBusService @@ -27,30 +42,70 @@ class ProductService extends BaseService { this.productVariantService_ = productVariantService } - /** - * Used to validate product ids. Throws an error if the cast fails - * @param {string} rawId - the raw product id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The productId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new ProductService({ + manager: transactionManager, + productRepository: this.productRepository_, + productVariantRepository: this.productVariantRepository_, + productOptionRepository: this.productOptionRepository_, + eventBusService: this.eventBus_, + productVariantService: this.productVariantService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** - * @param {Object} selector - the query object for find + * @param {Object} listOptions - the query object for find * @return {Promise} the result of the find operation */ - list(selector, offset, limit) { - return this.productModel_.find(selector, {}, offset, limit) + list(selector = {}, config = { relations: [], skip: 0, take: 20 }) { + const productRepo = this.manager_.getCustomRepository( + this.productRepository_ + ) + + let q + if ("q" in selector) { + q = selector.q + delete selector.q + } + + const query = this.buildQuery_(selector, config) + + if (q) { + const where = query.where + + delete where.description + delete where.title + + query.join = { + alias: "product", + leftJoinAndSelect: { + variant: "product.variants", + }, + } + + query.where = qb => { + qb.where(where) + + qb.andWhere( + new Brackets(qb => { + qb.where(`product.title ILIKE :q`, { q: `%${q}%` }) + .orWhere(`product.description ILIKE :q`, { q: `%${q}%` }) + .orWhere(`variant.title ILIKE :q`, { q: `%${q}%` }) + .orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` }) + }) + ) + } + } + + return productRepo.find(query) } /** @@ -58,30 +113,33 @@ class ProductService extends BaseService { * @return {Promise} the result of the count operation */ count() { - return this.productModel_.count() + const productRepo = this.manager_.getCustomRepository( + this.productRepository_ + ) + return productRepo.count() } /** * Gets a product by id. * Throws in case of DB Error and if product was not found. - * @param {string} productId - the id of the product to get. - * @return {Promise} the product document. + * @param {string} productId - id of the product to get. + * @return {Promise} the result of the find one operation. */ - async retrieve(productId) { - const validatedId = this.validateId_(productId) - const product = await this.productModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieve(productId, config = {}) { + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) + const validatedId = this.validateId_(productId) + const query = this.buildQuery_({ id: validatedId }, config) + const product = await productRepo.findOne(query) + if (!product) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Product with id: ${productId} was not found` + ) + } - if (!product) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Product with ${productId} was not found` - ) - } - return product + return product + }) } /** @@ -90,157 +148,125 @@ class ProductService extends BaseService { * @return {Promise} an array of variants */ async retrieveVariants(productId) { - const product = await this.retrieve(productId) - return this.productVariantService_.list({ _id: { $in: product.variants } }) + const product = await this.retrieve(productId, { relations: ["variants"] }) + return product.variants } /** - * Creates an unpublished product. - * @param {object} product - the product to create + * Creates a product. + * @param {object} productObject - the product to create * @return {Promise} resolves to the creation result. */ - async createDraft(product) { - return this.productModel_ - .create({ - ...product, - published: false, - }) - .then(result => { - this.eventBus_.emit(ProductService.Events.CREATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async create(productObject) { + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) + const optionRepo = manager.getCustomRepository( + this.productOptionRepository_ + ) + + const product = await productRepo.create(productObject) + + product.options = await Promise.all( + productObject.options.map(async o => { + const res = await optionRepo.create({ ...o, product_id: product.id }) + await optionRepo.save(res) + return res + }) + ) + + const result = await productRepo.save(product) + + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.CREATED, { + id: result.id, + }) + return result + }) } /** - * Creates an publishes product. - * @param {string} productId - ID of the product to publish. - * @return {Promise} resolves to the creation result. - */ - async publish(productId) { - return this.productModel_ - .updateOne({ _id: productId }, { $set: { published: true } }) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Updates a product. Metadata updates and product variant updates should - * use dedicated methods, e.g. `setMetadata`, `addVariant`, etc. The function - * will throw errors if metadata or product variant updates are attempted. + * Updates a product. Product variant updates should use dedicated methods, + * e.g. `addVariant`, etc. The function will throw errors if metadata or + * product variant updates are attempted. * @param {string} productId - the id of the product. 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(productId, update) { - const validatedId = this.validateId_(productId) - - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) + const productVariantRepo = manager.getCustomRepository( + this.productVariantRepository_ ) - } - if (update.variants) { - const existingVariants = await this.retrieveVariants(validatedId) - for (const existing of existingVariants) { - if (!update.variants.find(v => v._id && existing._id.equals(v._id))) { - await this.deleteVariant(productId, existing._id) - } + const product = await this.retrieve(productId, { + relations: ["variants"], + }) + + const { variants, metadata, options, images, ...rest } = update + + if (!product.thumbnail && !update.thumbnail && images && images.length) { + product.thumbnail = images[0] } - await Promise.all( - update.variants.map(async variant => { - if (variant._id) { - const variantFromDb = existingVariants.find(v => - v._id.equals(variant._id) - ) - if (variant.prices && variant.prices.length) { - // if equal we dont want to update - const isPricesEqual = compareObjectsByProp( - variant, - variantFromDb, - "prices" - ) + if (metadata) { + product.metadata = this.setMetadata_(product, metadata) + } - if (!isPricesEqual) { - for (const price of variant.prices) { - if (price.region_id) { - await this.productVariantService_.setRegionPrice( - variant._id, - price.region_id, - price.amount, - price.sale_amount || undefined - ) - } else { - await this.productVariantService_.setCurrencyPrice( - variant._id, - price.currency_code, - price.amount, - price.sale_amount || undefined - ) - } - } - } - } - - if (variant.options && variant.options.length) { - // if equal we dont want to update - const isOptionsEqual = compareObjectsByProp( - variant, - variantFromDb, - "options" - ) - - if (!isOptionsEqual) { - for (const option of variant.options) { - await this.updateOptionValue( - productId, - variant._id, - option.option_id, - option.value - ) - } - } - } - - delete variant.prices - delete variant.options - - if (!_.isEmpty(variant)) { - await this.productVariantService_.update(variant._id, variant) - } - } else { - await this.createVariant(productId, variant).then(res => res._id) + if (variants) { + // Iterate product variants and update their properties accordingly + for (const variant of product.variants) { + const exists = variants.find(v => v.id && variant.id === v.id) + if (!exists) { + await productVariantRepo.remove(variant) } + } + + const newVariants = [] + for (const newVariant of variants) { + if (newVariant.id) { + const variant = product.variants.find(v => v.id === newVariant.id) + + if (!variant) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Variant with id: ${newVariant.id} is not associated with this product` + ) + } + + const saved = await this.productVariantService_ + .withTransaction(manager) + .update(variant, newVariant) + + newVariants.push(saved) + } else { + // If the provided variant does not have an id, we assume that it + // should be created + const created = await this.productVariantService_ + .withTransaction(manager) + .create(product.id, newVariant) + + newVariants.push(created) + } + } + + product.variants = newVariants + } + + for (const [key, value] of Object.entries(rest)) { + product[key] = value + } + + const result = await productRepo.save(product) + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.UPDATED, { + id: result.id, }) - ) - - delete update.variants - } - - return this.productModel_ - .updateOne( - { _id: validatedId }, - { $set: update }, - { runValidators: true } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + return result + }) } /** @@ -248,88 +274,21 @@ class ProductService extends BaseService { * variants will also be deleted. * @param {string} productId - the id of the product to delete. Must be * castable as an ObjectId - * @return {Promise} the result of the delete operation. + * @return {Promise} empty promise */ async delete(productId) { - let product - try { - product = await this.retrieve(productId) - } catch (error) { - // Delete is idempotent, but we return a promise to allow then-chaining + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) + + // Should not fail, if product does not exist, since delete is idempotent + const product = await productRepo.findOne({ where: { id: productId } }) + + if (!product) return Promise.resolve() + + await productRepo.softRemove(product) + return Promise.resolve() - } - - await Promise.all( - product.variants.map(id => this.productVariantService_.delete(id)) - ).catch(err => { - throw err }) - - return this.productModel_.deleteOne({ _id: product._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Adds a product variant to a product. Will check that the given product - * variant has correct option values. - * @param {string} productId - the product the variant will be added to - * @param {string} variantId - the variant to add to the product - * @return {Promise} the result of update - */ - async createVariant(productId, variant) { - const product = await this.retrieve(productId) - - if (product.options.length !== variant.options.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Product options length does not match variant options length. Product has ${product.options.length} and variant has ${variant.options.length}.` - ) - } - - product.options.forEach(option => { - if (!variant.options.find(vo => option._id.equals(vo.option_id))) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Variant options do not contain value for ${option.title}` - ) - } - }) - - let combinationExists = false - if (product.variants && product.variants.length) { - const variants = await this.retrieveVariants(productId) - // Check if option value of the variant to add already exists. Go through - // each existing variant. Check if this variants option values are - // identical to the option values of the variant being added. - combinationExists = variants.some(v => { - return v.options.every(option => { - const variantOption = variant.options.find(o => - option.option_id.equals(o.option_id) - ) - return option.value === variantOption.value - }) - }) - } - - if (combinationExists) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Variant with provided options already exists` - ) - } - - const newVariant = await this.productVariantService_.createDraft(variant) - - return this.productModel_ - .updateOne({ _id: product._id }, { $push: { variants: newVariant._id } }) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) } /** @@ -341,108 +300,75 @@ class ProductService extends BaseService { * @return {Promise} the result of the model update operation */ async addOption(productId, optionTitle) { - const product = await this.retrieve(productId) - - // Make sure that option doesn't already exist - if (product.options.find(o => o.title === optionTitle)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `An option with the title: ${optionTitle} already exists` + return this.atomicPhase_(async manager => { + const productOptionRepo = manager.getCustomRepository( + this.productOptionRepository_ ) - } - const optionId = mongoose.Types.ObjectId() - - // All product variants must have at least a dummy value for the new option - if (product.variants) { - await Promise.all( - product.variants.map(async variantId => - this.productVariantService_.addOptionValue( - variantId, - optionId, - "Default Value" - ) - ) - ).catch(async err => { - // If any of the variants failed to add the new option value we clean up - return Promise.all( - product.variants.map(async variantId => - this.productVariantService_.deleteOptionValue(variantId, optionId) - ) - ).then(() => { - throw err - }) + const product = await this.retrieve(productId, { + relations: ["options", "variants"], }) - } - // Everything went well add the product option - return this.productModel_ - .updateOne( - { _id: productId }, - { - $push: { - options: { - _id: optionId, - title: optionTitle, - product_id: productId, - }, - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(async err => { - // If we failed to update the product clean up its variants - return Promise.all( - product.variants.map(async variantId => - this.productVariantService_.deleteOptionValue(variantId, optionId) - ) - ).then(() => { - throw err - }) - }) - } - - async reorderVariants(productId, variantOrder) { - const product = await this.retrieve(productId) - - if (product.variants.length !== variantOrder.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Product variants and new variant order differ in length. To delete or add variants use removeVariant or addVariant` - ) - } - - const newOrder = variantOrder.map(vId => { - const variant = product.variants.find(id => id === vId) - if (!variant) { + if (product.options.find(o => o.title === optionTitle)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `Product has no variant with id: ${vId}` + `An option with the title: ${optionTitle} already exists` ) } - return variant - }) + const option = await productOptionRepo.create({ + title: optionTitle, + product_id: productId, + }) - return this.productModel_ - .updateOne( - { - _id: productId, - }, - { - $set: { variants: newOrder }, + const result = await productOptionRepo.save(option) + + for (const variant of product.variants) { + this.productVariantService_ + .withTransaction(manager) + .addOptionValue(variant.id, option.id, "Default Value") + } + + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.UPDATED, result) + return result + }) + } + + async reorderVariants(productId, variantOrder) { + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) + + const product = await this.retrieve(productId, { + relations: ["variants"], + }) + + if (product.variants.length !== variantOrder.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product variants and new variant order differ in length.` + ) + } + + product.variants = variantOrder.map(vId => { + const variant = product.variants.find(v => v.id === vId) + if (!variant) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product has no variant with id: ${vId}` + ) } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + + return variant }) + + const result = productRepo.save(product) + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.UPDATED, result) + return result + }) } /** @@ -455,43 +381,36 @@ class ProductService extends BaseService { * @return {Promise} the result of the update operation */ async reorderOptions(productId, optionOrder) { - const product = await this.retrieve(productId) + return this.atomicPhase_(async manager => { + const productRepo = manager.getCustomRepository(this.productRepository_) - if (product.options.length !== optionOrder.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Product options and new options order differ in length. To delete or add options use removeOption or addOption` - ) - } + const product = await this.retrieve(productId, { relations: ["options"] }) - const newOrder = optionOrder.map(oId => { - const option = product.options.find(o => o._id === oId) - if (!option) { + if (product.options.length !== optionOrder.length) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `Product has no option with id: ${oId}` + `Product options and new options order differ in length.` ) } - return option - }) - - return this.productModel_ - .updateOne( - { - _id: productId, - }, - { - $set: { options: newOrder }, + product.options = optionOrder.map(oId => { + const option = product.options.find(o => o.id === oId) + if (!option) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product has no option with id: ${oId}` + ) } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + + return option }) + + const result = productRepo.save(product) + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.UPDATED, result) + return result + }) } /** @@ -500,68 +419,75 @@ class ProductService extends BaseService { * @param {string} productId - the product whose option we are updating * @param {string} optionId - the id of the option we are updating * @param {object} data - the data to update the option with - * @return {Promise} the result of the update operation + * @return {Promise} the updated product */ async updateOption(productId, optionId, data) { - const product = await this.retrieve(productId) - - const option = product.options.find(o => o._id.equals(optionId)) - if (!option) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Product has no option with id: ${optionId}` + return this.atomicPhase_(async manager => { + const productOptionRepo = manager.getCustomRepository( + this.productOptionRepository_ ) - } - const { title, values } = data - const titleExists = product.options.some( - o => - o.title.toUpperCase() === title.toUpperCase() && !o._id.equals(optionId) - ) + const product = await this.retrieve(productId, { relations: ["options"] }) - if (titleExists) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `An option with title ${title} already exists` + const { title, values } = data + + const optionExists = product.options.some( + o => o.title.toUpperCase() === title.toUpperCase() && o.id !== optionId ) - } + if (optionExists) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `An option with title ${title} already exists` + ) + } - const update = {} - update["options.$.title"] = title - - return this.productModel_ - .updateOne( - { - _id: productId, - "options._id": optionId, - }, - { - $set: update, - } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + const productOption = await productOptionRepo.findOne({ + where: { id: optionId }, }) + + if (!productOption) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Option with id: ${optionId} deos not exists` + ) + } + + productOption.title = title + productOption.values = values + + await productOptionRepo.save(productOption) + + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.UPDATED, product) + return product + }) } /** * Delete an option from a product. * @param {string} productId - the product to delete an option from * @param {string} optionId - the option to delete - * @return {Promise} return the result of update + * @return {Promise} the updated product */ async deleteOption(productId, optionId) { - const product = await this.retrieve(productId) + return this.atomicPhase_(async manager => { + const productOptionRepo = manager.getCustomRepository( + this.productOptionRepository_ + ) - if (!product.options.find(o => o._id.equals(optionId))) { - return Promise.resolve() - } + const product = await this.retrieve(productId, { + relations: ["variants", "variants.options"], + }) + + const productOption = await productOptionRepo.findOne({ + where: { id: optionId, product_id: productId }, + }) + + if (!productOption) { + return Promise.resolve() + } - if (product.variants.length) { // For the option we want to delete, make sure that all variants have the // same option values. The reason for doing is, that we want to avoid // duplicate variants. For example, if we have a product with size and @@ -570,17 +496,15 @@ class ProductService extends BaseService { // we would end up with four variants: (black), (black), (blue), (blue). // We now have two duplicate variants. To ensure that this does not // happen, we will force the user to select which variants to keep. - const firstVariant = await this.productVariantService_.retrieve( - product.variants[0] - ) - const valueToMatch = firstVariant.options.find(o => - o.option_id.equals(optionId) + const firstVariant = product.variants[0] + + const valueToMatch = firstVariant.options.find( + o => o.option_id === optionId ).value const equalsFirst = await Promise.all( - product.variants.map(async vId => { - const v = await this.productVariantService_.retrieve(vId) - const option = v.options.find(o => o.option_id.equals(optionId)) + product.variants.map(async v => { + const option = v.options.find(o => o.option_id === optionId) return option.value === valueToMatch }) ) @@ -588,121 +512,18 @@ class ProductService extends BaseService { if (!equalsFirst.every(v => v)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist. For more info check MEDUSA.com` + `To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist.` ) } - } - const result = await this.productModel_ - .updateOne( - { _id: productId }, - { - $pull: { - options: { - _id: optionId, - }, - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + // If we reach this point, we can safely delete the product option + await productOptionRepo.softRemove(productOption) - // If we reached this point, we can delete option value from variants - if (product.variants.length) { - await Promise.all( - product.variants.map(async variantId => - this.productVariantService_.deleteOptionValue(variantId, optionId) - ) - ) - } - - return result - } - - /** - * Removes variant from product - * @param {string} productId - the product to remove the variant from - * @param {string} variantId - the variant to remove from product - * @return {Promise} the result of update - */ - async deleteVariant(productId, variantId) { - const product = await this.retrieve(productId) - - await this.productVariantService_.delete(variantId) - - return this.productModel_ - .updateOne( - { _id: product._id }, - { - $pull: { - variants: variantId, - }, - } - ) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - async updateOptionValue(productId, variantId, optionId, value) { - const product = await this.retrieve(productId) - - // Check if the product-to-variant relationship holds - if (!product.variants.includes(variantId)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The variant could not be found in the product" - ) - } - - // Retrieve all variants - const variants = await this.retrieveVariants(productId) - const toUpdate = variants.find(v => v._id.equals(variantId)) - - // Check if an update would create duplicate variants - const canUpdate = variants.every(v => { - // The variant we update is irrelevant - if (v._id.equals(variantId)) { - return true - } - - // Check if the variant's options are identical to the variant we - // are updating - const hasMatchingOptions = v.options.every(option => { - if (option.option_id.equals(optionId)) { - return option.value === value - } - - const toUpdateOption = toUpdate.options.find(o => - option.option_id.equals(o.option_id) - ) - return toUpdateOption.value === option.value - }) - - return !hasMatchingOptions + await this.eventBus_ + .withTransaction(manager) + .emit(ProductService.Events.UPDATED, product) + return product }) - - if (!canUpdate) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "A variant with the given option value combination already exist" - ) - } - - return this.productVariantService_.updateOptionValue( - variantId, - optionId, - value - ) } /** @@ -712,69 +533,18 @@ class ProductService extends BaseService { * @param {string[]} expandFields - fields to expand. * @return {Product} return the decorated product. */ - async decorate(product, fields, expandFields = []) { - const requiredFields = ["_id", "metadata"] - const decorated = _.pick(product, fields.concat(requiredFields)) - if (expandFields.includes("variants")) { - decorated.variants = await this.retrieveVariants(product._id) - } - const final = await this.runDecorators_(decorated) - return final - } + async decorate(productId, fields = [], expandFields = []) { + const requiredFields = ["id", "metadata"] - /** - * Dedicated method to set metadata for a product. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} productId - the product to decorate. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(productId, key, value) { - const validatedId = this.validateId_(productId) + fields = fields.concat(requiredFields) - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } + const product = await this.retrieve(productId, { + select: fields, + relations: expandFields, + }) - const keyPath = `metadata.${key}` - return this.productModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .then(result => { - this.eventBus_.emit(ProductService.Events.UPDATED, result) - return result - }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for a product. - * @param {string} productId - the product to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(productId, key) { - const validatedId = this.validateId_(productId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.productModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + // const final = await this.runDecorators_(decorated) + return product } } diff --git a/packages/medusa/src/services/region.js b/packages/medusa/src/services/region.js index c8681b9589..41aa1732f4 100644 --- a/packages/medusa/src/services/region.js +++ b/packages/medusa/src/services/region.js @@ -9,19 +9,39 @@ import { countries } from "../utils/countries" */ class RegionService extends BaseService { constructor({ - regionModel, + manager, + regionRepository, + countryRepository, storeService, + currencyRepository, + paymentProviderRepository, + fulfillmentProviderRepository, paymentProviderService, fulfillmentProviderService, }) { super() - /** @private @const {RegionModel} */ - this.regionModel_ = regionModel + /** @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 {CurrencyRepository} */ + this.currencyRepository_ = currencyRepository + + /** @private @const {PaymentProviderRepository} */ + this.paymentProviderRepository_ = paymentProviderRepository + + /** @private @const {FulfillmentProviderRepository} */ + this.fulfillmentProviderRepository_ = fulfillmentProviderRepository + /** @private @const {PaymentProviderService} */ this.paymentProviderService_ = paymentProviderService @@ -29,33 +49,126 @@ class RegionService extends BaseService { this.fulfillmentProviderService_ = fulfillmentProviderService } + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new RegionService({ + manager: transactionManager, + regionRepository: this.regionRepository_, + countryRepository: this.countryRepository_, + storeService: this.storeService_, + paymentProviderRepository: this.paymentProviderRepository_, + paymentProviderService: this.paymentProviderService_, + fulfillmentProviderRepository: this.fulfillmentProviderRepository_, + fulfillmentProviderService: this.fulfillmentProviderService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + /** * Creates a region. * @param {Region} rawRegion - the unvalidated region * @return {Region} the newly created region */ - async create(rawRegion) { - const region = await this.validateFields_(rawRegion) - return this.regionModel_.create(region) + async create(regionObject) { + return this.atomicPhase_(async manager => { + const regionRepository = manager.getCustomRepository( + this.regionRepository_ + ) + const currencyRepository = manager.getCustomRepository( + this.currencyRepository_ + ) + + const { metadata, currency_code, ...toValidate } = regionObject + + const validated = await this.validateFields_(toValidate) + + if (currency_code) { + // will throw if currency is not added to store currencies + await this.validateCurrency_(currency_code) + const currency = await currencyRepository.findOne({ + where: { code: currency_code.toLowerCase() }, + }) + + if (!currency) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Could not find currency with code ${currency_code}` + ) + } + + regionObject.currency = currency + regionObject.currency_code = currency_code.toLowerCase() + } + + if (metadata) { + regionObject.metadata = this.setMetadata_(region, metadata) + } + + for (const [key, value] of Object.entries(validated)) { + regionObject[key] = value + } + + const created = await regionRepository.create(regionObject) + const result = await regionRepository.save(created) + return result + }) } /** - * Updates a region. Note metadata cannot be set with the update function, use - * setMetadata instead. + * 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 */ async update(regionId, update) { - const region = await this.validateFields_(update, regionId) - return this.regionModel_.updateOne( - { - _id: regionId, - }, - { - $set: region, + return this.atomicPhase_(async manager => { + const regionRepository = manager.getCustomRepository( + this.regionRepository_ + ) + const currencyRepository = manager.getCustomRepository( + this.currencyRepository_ + ) + + const region = await this.retrieve(regionId) + + const { metadata, currency_code, ...toValidate } = update + + 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) + const currency = await currencyRepository.findOne({ + where: { code: currency_code.toLowerCase() }, + }) + + if (!currency) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Could not find currency with code ${currency_code}` + ) + } + + region.currency_code = currency_code.toLowerCase() } - ) + + if (metadata) { + region.metadata = this.setMetadata_(region, metadata) + } + + for (const [key, value] of Object.entries(validated)) { + region[key] = value + } + + const result = await regionRepository.save(region) + return result + }) } /** @@ -66,15 +179,17 @@ class RegionService extends BaseService { * @return {object} the validated region data */ async validateFields_(region, id = undefined) { + const ppRepository = this.manager_.getCustomRepository( + this.paymentProviderRepository_ + ) + const fpRepository = this.manager_.getCustomRepository( + this.fulfillmentProviderRepository_ + ) + if (region.tax_rate) { this.validateTaxRate_(region.tax_rate) } - if (region.currency_code) { - region.currency_code = region.currency_code.toUpperCase() - await this.validateCurrency_(region.currency_code) - } - if (region.countries) { region.countries = await Promise.all( region.countries.map(countryCode => @@ -85,25 +200,36 @@ class RegionService extends BaseService { }) } - if (region.metadata) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Please use setMetadata" + if (region.payment_providers) { + region.payment_providers = await Promise.all( + region.payment_providers.map(async pId => { + const pp = await ppRepository.findOne({ where: { id: pId } }) + if (!pp) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Payment provider not found" + ) + } + + return pp + }) ) } if (region.fulfillment_providers) { - // Will throw if we do not find the provider - region.fulfillment_providers.forEach(pId => { - this.fulfillmentProviderService_.retrieveProvider(pId) - }) - } + region.fulfillment_providers = await Promise.all( + region.fulfillment_providers.map(async fId => { + const fp = await fpRepository.findOne({ where: { id: fId } }) + if (!fp) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Fulfillment provider not found" + ) + } - if (region.payment_providers) { - // Will throw if we do not find the provider - region.payment_providers.forEach(pId => { - this.paymentProviderService_.retrieveProvider(pId) - }) + return fp + }) + ) } return region @@ -114,7 +240,7 @@ class RegionService extends BaseService { * @param {number} taxRate - a number representing the tax rate of the region */ validateTaxRate_(taxRate) { - if (taxRate > 1 || taxRate < 0) { + if (taxRate > 100 || taxRate < 0) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "The tax_rate must be between 0 and 1" @@ -127,9 +253,11 @@ class RegionService extends BaseService { * @param {string} currencyCode - an ISO currency code */ async validateCurrency_(currencyCode) { - const store = await this.storeService_.retrieve() + const store = await this.storeService_.retrieve(["currencies"]) - if (!store.currencies.includes(currencyCode.toUpperCase())) { + const storeCurrencies = store.currencies.map(curr => curr.code) + + if (!storeCurrencies.includes(currencyCode.toLowerCase())) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Invalid currency code" @@ -141,45 +269,43 @@ 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} id - the id of the current region to check against + * @param {string} regionId - the id of the current region to check against */ - async validateCountry_(code, id) { + async validateCountry_(code, regionId) { + const countryRepository = this.manager_.getCustomRepository( + this.countryRepository_ + ) + const countryCode = code.toUpperCase() - const country = countries.find(c => c.alpha2 === countryCode) - if (!country) { + const validCountry = countries.find(c => c.alpha2 === countryCode) + if (!validCountry) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Invalid country code" ) } - const existing = await this.regionModel_.findOne({ countries: countryCode }) - if (existing && !existing._id.equals(id)) { + const country = await countryRepository.findOne({ + where: { + iso_2: code.toLowerCase(), + }, + }) + + if (!country) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Country with code ${code} not found` + ) + } + + if (country.region_id && country.region_id !== regionId) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, - `${country.name} already exists in ${existing.name}, delete it in that region before adding it` + `${country.name} already exists in ${country.name}, delete it in that region before adding it` ) } - return countryCode - } - - /** - * Used to validate region ids. Throws an error if the cast fails - * @param {string} rawId - the raw region id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The regionId could not be casted to an ObjectId" - ) - } - - return value + return country } /** @@ -187,9 +313,14 @@ class RegionService extends BaseService { * @param {string} regionId - the id of the region to retrieve * @return {Region} the region */ - async retrieve(regionId) { + async retrieve(regionId, config = {}) { + const regionRepository = this.manager_.getCustomRepository( + this.regionRepository_ + ) + const validatedId = this.validateId_(regionId) - const region = await this.regionModel_.findOne({ _id: validatedId }) + const query = this.buildQuery_({ id: validatedId }, config) + const region = await regionRepository.findOne(query) if (!region) { throw new MedusaError( @@ -202,12 +333,14 @@ class RegionService extends BaseService { /** * Lists all regions based on a query - * @param {string} regionId - the id of the region to retrieve - * @return {Region} the region + * @param {object} listOptions - query object for find + * @return {Promise} result of the find operation */ - async list(query) { - const regions = await this.regionModel_.find(query) - return regions + async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { + const regionRepo = this.manager_.getCustomRepository(this.regionRepository_) + + const query = this.buildQuery_(selector, config) + return regionRepo.find(query) } /** @@ -215,9 +348,17 @@ class RegionService extends BaseService { * @param {string} regionId - the region to delete * @return {Promise} the result of the delete operation */ - delete(regionId) { - return this.regionModel_.deleteOne({ - _id: regionId, + async delete(regionId) { + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) + + const region = await regionRepo.findOne({ where: { id: regionId } }) + + if (!region) return Promise.resolve() + + await regionRepo.softRemove(region) + + return Promise.resolve() }) } @@ -228,21 +369,26 @@ class RegionService extends BaseService { * @return {Promise} the result of the update operation */ async addCountry(regionId, code) { - const region = await this.retrieve(regionId) - const countryCode = await this.validateCountry_(code, regionId) + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) - if (region.countries.includes(countryCode)) { - return Promise.resolve() - } + const country = await this.validateCountry_(code, regionId) - return this.regionModel_.updateOne( - { - _id: region._id, - }, - { - $push: { countries: countryCode }, + const region = await this.retrieve(regionId, { relations: ["countries"] }) + + // Check if region already has country + if ( + region.countries && + region.countries.map(c => c.iso_2).includes(country.iso_2) + ) { + return region } - ) + + region.countries = [...(region.countries || []), country] + + const updated = await regionRepo.save(region) + return updated + }) } /** @@ -252,17 +398,26 @@ class RegionService extends BaseService { * @return {Promise} the result of the update operation */ async removeCountry(regionId, code) { - const countryCode = code.toUpperCase() - const region = await this.retrieve(regionId) + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) - return this.regionModel_.updateOne( - { _id: region._id }, - { - $pull: { - countries: countryCode, - }, + const region = await this.retrieve(regionId, { relations: ["countries"] }) + + // Check if region contains country. If not, we simpy resolve + if ( + region.countries && + !region.countries.map(c => c.iso_2).includes(code) + ) { + return region } - ) + + region.countries = region.countries.filter( + country => country.iso_2 !== code + ) + + const updated = await regionRepo.save(region) + return updated + }) } /** @@ -273,23 +428,35 @@ class RegionService extends BaseService { * @return {Promise} the result of the update operation */ async addPaymentProvider(regionId, providerId) { - const region = await this.retrieve(regionId) + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) + const ppRepo = manager.getCustomRepository( + this.paymentProviderRepository_ + ) - if (region.payment_providers.includes(providerId)) { - return Promise.resolve() - } + const region = await this.retrieve(regionId, { + relations: ["payment_providers"], + }) - // Will throw if we do not find the provider - this.paymentProviderService_.retrieveProvider(providerId) - - return this.regionModel_.updateOne( - { - _id: region._id, - }, - { - $push: { payment_providers: providerId }, + // Check if region already has payment provider + if (region.payment_providers.find(({ id }) => id === providerId)) { + return region } - ) + + const pp = await ppRepo.findOne({ where: { id: providerId } }) + + if (!pp) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Payment provider ${providerId} was not found` + ) + } + + region.payment_providers = [...region.payment_providers, pp] + + const updated = await regionRepo.save(region) + return updated + }) } /** @@ -300,23 +467,35 @@ class RegionService extends BaseService { * @return {Promise} the result of the update operation */ async addFulfillmentProvider(regionId, providerId) { - const region = await this.retrieve(regionId) + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) + const fpRepo = manager.getCustomRepository( + this.fulfillmentProviderRepository_ + ) - if (region.fulfillment_providers.includes(providerId)) { - return Promise.resolve() - } + const region = await this.retrieve(regionId, { + relations: ["fulfillment_providers"], + }) - // Will throw if we do not find the provider - this.fulfillmentProviderService_.retrieveProvider(providerId) - - return this.regionModel_.updateOne( - { - _id: region._id, - }, - { - $push: { fulfillment_providers: providerId }, + // Check if region already has payment provider + if (region.fulfillment_providers.find(({ id }) => id === providerId)) { + return Promise.resolve() } - ) + + const fp = await fpRepo.findOne({ where: { id: providerId } }) + + if (!fp) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Fulfillment provider ${providerId} was not found` + ) + } + + region.fulfillment_providers = [...region.fulfillment_providers, fp] + + const updated = await regionRepo.save(region) + return updated + }) } /** @@ -326,16 +505,25 @@ class RegionService extends BaseService { * @return {Promise} the result of the update operation */ async removePaymentProvider(regionId, providerId) { - const region = await this.retrieve(regionId) + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) - return this.regionModel_.updateOne( - { _id: region._id }, - { - $pull: { - payment_providers: providerId, - }, + const region = await this.retrieve(regionId, { + relations: ["payment_providers"], + }) + + // Check if region already has payment provider + if (!region.payment_providers.find(({ id }) => id === providerId)) { + return Promise.resolve() } - ) + + region.payment_providers = region.payment_providers.filter( + ({ id }) => id !== providerId + ) + + const updated = await regionRepo.save(region) + return updated + }) } /** @@ -345,16 +533,25 @@ class RegionService extends BaseService { * @return {Promise} the result of the update operation */ async removeFulfillmentProvider(regionId, providerId) { - const region = await this.retrieve(regionId) + return this.atomicPhase_(async manager => { + const regionRepo = manager.getCustomRepository(this.regionRepository_) - return this.regionModel_.updateOne( - { _id: region._id }, - { - $pull: { - fulfillment_providers: providerId, - }, + const region = await this.retrieve(regionId, { + relations: ["fulfillment_providers"], + }) + + // Check if region already has payment provider + if (!region.fulfillment_providers.find(({ id }) => id === providerId)) { + return Promise.resolve() } - ) + + region.fulfillment_providers = region.fulfillment_providers.filter( + ({ id }) => id !== providerId + ) + + const updated = await regionRepo.save(region) + return updated + }) } /** @@ -365,62 +562,11 @@ class RegionService extends BaseService { * @return {Region} the region */ async decorate(region, fields, expandFields = []) { - const requiredFields = ["_id", "metadata"] + const requiredFields = ["id", "metadata"] const decorated = _.pick(region, fields.concat(requiredFields)) const final = await this.runDecorators_(decorated) return final } - - /** - * Dedicated method to set metadata for a region. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} regionId - the region to decorate. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(regionId, key, value) { - const validatedId = this.validateId_(regionId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.regionModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for an region. - * @param {string} regionId - the region to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(regionId, key) { - const validatedId = this.validateId_(regionId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.regionModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } } export default RegionService diff --git a/packages/medusa/src/services/return.js b/packages/medusa/src/services/return.js index 34a4fc7db7..539ed8ff36 100644 --- a/packages/medusa/src/services/return.js +++ b/packages/medusa/src/services/return.js @@ -8,20 +8,58 @@ import { MedusaError } from "medusa-core-utils" */ class ReturnService extends BaseService { constructor({ + manager, totalsService, + lineItemService, + returnRepository, + returnItemRepository, shippingOptionService, fulfillmentProviderService, }) { 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 + + /** @private @const {ShippingOptionService} */ this.shippingOptionService_ = shippingOptionService + /** @private @const {FulfillmentProviderService} */ this.fulfillmentProviderService_ = fulfillmentProviderService } + withTransaction(transactionManager) { + if (!transactionManager) { + return this + } + + const cloned = new ReturnService({ + manager: transactionManager, + totalsService: this.totalsService_, + lineItemService: this.lineItemService_, + returnRepository: this.returnRepository_, + returnItemRepository: this.returnItemRepository_, + shippingOptionService: this.shippingOptionService_, + fulfillmentProviderService: this.fulfillmentProviderService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + /** * Retrieves the order line items, given an array of items. * @param {Order} order - the order to get line items from @@ -35,7 +73,7 @@ class ReturnService extends BaseService { async getFulfillmentItems_(order, items, transformer) { const toReturn = await Promise.all( items.map(async ({ item_id, quantity }) => { - const item = order.items.find(i => i._id.equals(item_id)) + const item = order.items.find(i => i.id === item_id) return transformer(item, quantity) }) ) @@ -43,6 +81,19 @@ class ReturnService extends BaseService { return toReturn.filter(i => !!i) } + /** + * @param {Object} selector - the query object for find + * @return {Promise} the result of the find operation + */ + list( + selector, + config = { skip: 0, take: 50, order: { created_at: "DESC" } } + ) { + const returnRepo = this.manager_.getCustomRepository(this.returnRepository_) + const query = this.buildQuery_(selector, config) + return returnRepo.find(query) + } + /** * Checks that an order has the statuses necessary to complete a return. * fulfillment_status cannot be not_fulfilled or returned. @@ -102,88 +153,200 @@ class ReturnService extends BaseService { } /** - * Creates a return request for an order, with given items, and a shipping - * method. If no refundAmount is provided the refund amount is calculated from - * the return lines and the shipping cost. - * @param {String} orderId - the id of the order to create a return for. - * @param {Array<{item_id: String, quantity: Int}>} items - the line items to - * return - * @param {ShippingMethod?} shippingMethod - the shipping method used for the - * return - * @param {Number?} refundAmount - the amount to refund when the return is - * received. - * @returns {Promise} the resulting order. + * Retrieves a return by its id. + * @param {string} id - the id of the return to retrieve + * @return {Return} the return */ - async requestReturn(order, items, shippingMethod, refundAmount) { - // Throws if the order doesn't have the necessary status for return - this.validateReturnStatuses_(order) - - const returnLines = await this.getFulfillmentItems_( - order, - items, - this.validateReturnLineItem_ + async retrieve(id, config = {}) { + const returnRepository = this.manager_.getCustomRepository( + this.returnRepository_ ) - let toRefund = refundAmount - if (typeof refundAmount !== "undefined") { - const total = await this.totalsService_.getTotal(order) - const refunded = await this.totalsService_.getRefundedTotal(order) - const refundable = total - refunded - if (refundAmount > refundable) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Cannot refund more than the original payment" - ) - } - } else { - toRefund = await this.totalsService_.getRefundTotal(order, returnLines) + const validatedId = this.validateId_(id) + const query = this.buildQuery_({ id: validatedId }, config) + + const returnObj = await returnRepository.findOne(query) + + if (!returnObj) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Return with id: ${id} was not found` + ) } + return returnObj + } - let fulfillmentData = {} - let shipping_method = {} - if (typeof shippingMethod !== "undefined") { - shipping_method = await this.shippingOptionService_.retrieve( - shippingMethod.id + async retrieveBySwap(swapId, relations = []) { + const returnRepository = this.manager_.getCustomRepository( + this.returnRepository_ + ) + + const validatedId = this.validateId_(swapId) + + const returnObj = await returnRepository.findOne({ + where: { + swap_id: validatedId, + }, + relations, + }) + + if (!returnObj) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Return with swa_id: ${swapId} was not found` ) - const provider = await this.fulfillmentProviderService_.retrieveProvider( - shipping_method.provider_id - ) - fulfillmentData = await provider.createReturn( - shipping_method.data, - returnLines, - order + } + return returnObj + } + + async update(returnId, update) { + return this.atomicPhase_(async manager => { + const ret = await this.retrieve(returnId) + + const { metadata, ...rest } = update + + if ("metadata" in update) { + ret.metadata = this.setMetadata_(ret, update.metadata) + } + + for (const [key, value] of Object.entries(rest)) { + ret[key] = value + } + + const retRepo = manager.getCustomRepository(this.returnRepository_) + const result = await retRepo.save(ret) + return result + }) + } + + /** + * 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, + * items or refund_amount + * @param {object} orderLike - order object + * @returns {Promise} the resulting order. + */ + async create(data, orderLike) { + return this.atomicPhase_(async manager => { + const returnRepository = manager.getCustomRepository( + this.returnRepository_ ) - if (typeof shippingMethod.price !== "undefined") { - shipping_method.price = shippingMethod.price + const returnLines = await this.getFulfillmentItems_( + orderLike, + data.items, + this.validateReturnLineItem_ + ) + + let toRefund = data.refund_amount + if (typeof toRefund !== "undefined") { + const refundable = orderLike.total - orderLike.refunded_total + if (toRefund > refundable) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Cannot refund more than the original payment" + ) + } } else { - shipping_method.price = await this.shippingOptionService_.getPrice( - shipping_method, - { - ...order, - items: returnLines, - } + toRefund = await this.totalsService_.getRefundTotal( + orderLike, + returnLines + ) + + if (data.shipping_method) { + toRefund = Math.max( + 0, + toRefund - + data.shipping_method.price * (1 + orderLike.tax_rate / 100) + ) + } + } + + const method = data.shipping_method + delete data.shipping_method + + const returnObject = { + ...data, + status: "requested", + refund_amount: Math.floor(toRefund), + } + + const rItemRepo = manager.getCustomRepository(this.returnItemRepository_) + returnObject.items = returnLines.map(i => + rItemRepo.create({ + item_id: i.id, + quantity: i.quantity, + requested_quantity: i.quantity, + metadata: i.metadata, + }) + ) + + const created = await returnRepository.create(returnObject) + const result = await returnRepository.save(created) + + if (method) { + await this.shippingOptionService_ + .withTransaction(manager) + .createShippingMethod( + method.option_id, + {}, + { + price: method.price, + return_id: result.id, + } + ) + } + + return result + }) + } + + fulfill(returnId) { + return this.atomicPhase_(async manager => { + const returnOrder = await this.retrieve(returnId, { + relations: [ + "items", + "shipping_method", + "shipping_method.shipping_option", + "swap", + ], + }) + + const items = await this.lineItemService_.list({ + id: returnOrder.items.map(({ item_id }) => item_id), + }) + + returnOrder.items = returnOrder.items.map(item => { + const found = items.find(i => i.id === item.item_id) + return { + ...item, + item: found, + } + }) + + if (returnOrder.shipping_data) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Return has already been fulfilled" ) } - toRefund = Math.max( - 0, - toRefund - shipping_method.price * (1 + order.tax_rate) - ) - } + if (returnOrder.shipping_method === null) { + return returnOrder + } - return { - shipping_method, - refund_amount: toRefund, - items: returnLines.map(i => ({ - item_id: i._id, - content: i.content, - quantity: i.quantity, - is_requested: true, - metadata: i.metadata, - })), - shipping_data: fulfillmentData, - } + const fulfillmentData = await this.fulfillmentProviderService_.createReturn( + returnOrder + ) + + returnOrder.shipping_data = fulfillmentData + + const returnRepo = manager.getCustomRepository(this.returnRepository_) + const result = await returnRepo.save(returnOrder) + return result + }) } /** @@ -199,75 +362,99 @@ class ReturnService extends BaseService { * @return {Promise} the result of the update operation */ async receiveReturn( - order, - returnRequest, - items, + returnId, + receivedItems, refundAmount, allowMismatch = false ) { - if (returnRequest.status === "received") { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `Return with id ${returnId} has already been received` + return this.atomicPhase_(async manager => { + const returnRepository = manager.getCustomRepository( + this.returnRepository_ ) - } - const returnLines = await this.getFulfillmentItems_( - order, - items, - this.validateReturnLineItem_ - ) + const returnObj = await this.retrieve(returnId, { + relations: [ + "items", + "order", + "order.items", + "order.discounts", + "order.refunds", + "order.shipping_methods", + "order.region", + "swap", + "swap.order", + "swap.order.items", + "swap.order.refunds", + "swap.order.shipping_methods", + "swap.order.region", + ], + }) - const newLines = returnLines.map(l => { - const existing = returnRequest.items.find(i => l._id.equals(i.item_id)) - if (existing) { - return { - ...existing, - quantity: l.quantity, - requested_quantity: existing.quantity, - is_requested: l.quantity === existing.quantity, - is_registered: true, - } - } else { - return { - item_id: l._id, - content: l.content, - quantity: l.quantity, - is_requested: false, - is_registered: true, - metadata: l.metadata, - } + const order = returnObj.order || (returnObj.swap && returnObj.swap.order) + + if (returnObj.status === "received") { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Return with id ${returnId} has already been received` + ) } + + const returnLines = await this.getFulfillmentItems_( + order, + receivedItems, + this.validateReturnLineItem_ + ) + + const newLines = returnLines.map(l => { + const existing = returnObj.items.find(i => l.id === i.item_id) + if (existing) { + return { + ...existing, + quantity: l.quantity, + requested_quantity: existing.quantity, + received_quantity: l.quantity, + is_requested: l.quantity === existing.quantity, + } + } else { + return { + return_id: returnObj.id, + item_id: l.id, + quantity: l.quantity, + is_requested: false, + received_quantity: l.quantity, + metadata: l.metadata || {}, + } + } + }) + + let returnStatus = "received" + + const isMatching = newLines.every(l => l.is_requested) + if (!isMatching && !allowMismatch) { + // Should update status + returnStatus = "requires_action" + } + + const toRefund = refundAmount || returnObj.refund_amount + const total = await this.totalsService_.getTotal(order) + const refunded = await this.totalsService_.getRefundedTotal(order) + + if (toRefund > total - refunded) { + returnStatus = "requires_action" + } + + const now = new Date() + const updateObj = { + ...returnObj, + status: returnStatus, + items: newLines, + refund_amount: toRefund, + received_at: now.toISOString(), + } + + const result = await returnRepository.save(updateObj) + return result }) - - const isMatching = newLines.every(l => l.is_requested) - if (!isMatching && !allowMismatch) { - // Should update status - return { - ...returnRequest, - status: "requires_action", - items: newLines, - } - } - - const toRefund = refundAmount || returnRequest.refund_amount - const total = await this.totalsService_.getTotal(order) - const refunded = await this.totalsService_.getRefundedTotal(order) - - if (toRefund > total - refunded) { - return { - ...returnRequest, - status: "requires_action", - items: newLines, - } - } - - return { - ...returnRequest, - status: "received", - items: newLines, - refund_amount: toRefund, - } } } diff --git a/packages/medusa/src/services/shipping-option.js b/packages/medusa/src/services/shipping-option.js index 73cd79c002..11f82802e4 100644 --- a/packages/medusa/src/services/shipping-option.js +++ b/packages/medusa/src/services/shipping-option.js @@ -1,24 +1,35 @@ -import mongoose from "mongoose" import _ from "lodash" -import { Validator, MedusaError } from "medusa-core-utils" +import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { In } from "typeorm" /** * Provides layer to manipulate profiles. * @implements BaseService */ class ShippingOptionService extends BaseService { - /** @param { shippingOptionModel: (ShippingOptionModel) } */ constructor({ - shippingOptionModel, + manager, + shippingOptionRepository, + shippingOptionRequirementRepository, + shippingMethodRepository, fulfillmentProviderService, regionService, totalsService, }) { super() - /** @private @const {ShippingProfileModel} */ - this.optionModel_ = shippingOptionModel + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {ShippingOptionRepository} */ + this.optionRepository_ = shippingOptionRepository + + /** @private @const {ShippingMethodRepository} */ + this.methodRepository_ = shippingMethodRepository + + /** @private @const {ShippingOptionRequirementRepository} */ + this.requirementRepository_ = shippingOptionRequirementRepository /** @private @const {ProductService} */ this.providerService_ = fulfillmentProviderService @@ -30,22 +41,24 @@ class ShippingOptionService extends BaseService { this.totalsService_ = totalsService } - /** - * Used to validate product ids. Throws an error if the cast fails - * @param {string} rawId - the raw product id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The shippingOptionId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new ShippingOptionService({ + manager: transactionManager, + shippingOptionRepository: this.optionRepository_, + shippingMethodRepository: this.methodRepository_, + shippingOptionRequirementRepository: this.requirementRepository_, + fulfillmentProviderService: this.providerService_, + regionService: this.regionService_, + totalsService: this.totalsService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -53,7 +66,7 @@ class ShippingOptionService extends BaseService { * @param {ShippingRequirement} requirement - the requirement to validate * @return {ShippingRequirement} a validated shipping requirement */ - validateRequirement_(requirement) { + async validateRequirement_(requirement, optionId) { if (!requirement.type) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -71,25 +84,39 @@ class ShippingOptionService extends BaseService { ) } - return requirement + const reqRepo = this.manager_.getCustomRepository( + this.requirementRepository_ + ) + + const existingReq = await reqRepo.findOne({ + where: { id: requirement.id }, + }) + + let req + if (existingReq) { + req = await reqRepo.save({ + ...existingReq, + ...requirement, + }) + } else { + req = await reqRepo.create({ + shipping_option_id: optionId, + ...requirement, + }) + } + + return req } /** * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector) { - const query = {} + async list(selector, config = { skip: 0, take: 50 }) { + const optRepo = this.manager_.getCustomRepository(this.optionRepository_) - if (selector.region_id !== undefined) { - query.region_id = selector.region_id - } - - if ("is_return" in selector) { - query.is_return = selector.is_return.toLowerCase() === "true" - } - - return this.optionModel_.find(query) + const query = this.buildQuery_(selector, config) + return optRepo.find(query) } /** @@ -98,13 +125,23 @@ class ShippingOptionService extends BaseService { * @param {string} optionId - the id of the profile to get. * @return {Promise} the profile document. */ - async retrieve(optionId) { + async retrieve(optionId, options = {}) { + const soRepo = this.manager_.getCustomRepository(this.optionRepository_) const validatedId = this.validateId_(optionId) - const option = await this.optionModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + + const query = { + where: { id: validatedId }, + } + + if (options.select) { + query.select = options.select + } + + if (options.relations) { + query.relations = options.relations + } + + const option = await soRepo.findOne(query) if (!option) { throw new MedusaError( @@ -117,18 +154,107 @@ class ShippingOptionService extends BaseService { } /** - * Checks if the provided data for a fulfillment is valid. - * @param {string} optionId - the id of the option. - * @param {FulfillmentData} data - the data to validate - * @param {Cart} cart - the cart to validate against - * @return {FulfillmentData} the validated fulfillment data + * Updates a shipping method's associations. Useful when a cart is completed + * and its methods should be copied to an order/swap entity. + * @param {string} id - the id of the shipping method to update + * @param {object} update - the values to update the method with + * @returns {Promise} the resulting shipping method */ - async validateFulfillmentData(optionId, data, cart) { - const option = await this.retrieve(optionId) - const provider = await this.providerService_.retrieveProvider( - option.provider_id - ) - return provider.validateFulfillmentData(data, cart) + async updateShippingMethod(id, update) { + return this.atomicPhase_(async manager => { + const methodRepo = manager.getCustomRepository(this.methodRepository_) + const method = await methodRepo.findOne({ where: { id } }) + + if ("return_id" in update) { + method.return_id = update.return_id + } + + if ("swap_id" in update) { + method.swap_id = update.swap_id + } + + if ("order_id" in update) { + method.order_id = update.order_id + } + + return methodRepo.save(method) + }) + } + + /** + * Removes a given shipping method + * @param {string} id - the id of the option to use for the method. + */ + async deleteShippingMethod(sm) { + return this.atomicPhase_(async manager => { + const methodRepo = manager.getCustomRepository(this.methodRepository_) + return methodRepo.remove(sm) + }) + } + + /** + * Creates a shipping method for a given cart. + * @param {string} optionId - the id of the option to use for the method. + * @param {object} data - the optional provider data to use. + * @param {object} config - the cart to create the shipping method for. + * @returns {ShippingMethod} the resulting shipping method. + */ + async createShippingMethod(optionId, data, config) { + return this.atomicPhase_(async manager => { + const option = await this.retrieve(optionId, { + relations: ["requirements"], + }) + + const methodRepo = manager.getCustomRepository(this.methodRepository_) + + if ("cart" in config) { + this.validateCartOption(option, config.cart || {}) + } + + const validatedData = await this.providerService_.validateFulfillmentData( + option, + data, + config.cart || {} + ) + + let methodPrice + if ("price" in config) { + methodPrice = config.price + } else { + methodPrice = await this.getPrice_(option, validatedData, config.cart) + } + + const toCreate = { + shipping_option_id: option.id, + data: validatedData, + price: methodPrice, + } + + if (config.order) { + toCreate.order_id = config.order.id + } + + if (config.cart) { + toCreate.cart_id = config.cart.id + } + + if (config.return_id) { + toCreate.return_id = config.return_id + } + + if (config.order_id) { + toCreate.order_id = config.order_id + } + + const method = await methodRepo.create(toCreate) + + const created = await methodRepo.save(method) + + return methodRepo.findOne({ + where: { id: created.id }, + relations: ["shipping_option"], + }) + }) } /** @@ -139,9 +265,7 @@ class ShippingOptionService extends BaseService { * @param {Cart} cart - the cart object to check against * @return {ShippingOption} the validated shipping option */ - async validateCartOption(optionId, cart) { - const option = await this.retrieve(optionId) - + validateCartOption(option, cart) { if (option.is_return) { return null } @@ -153,26 +277,25 @@ class ShippingOptionService extends BaseService { ) } - const subtotal = this.totalsService_.getSubtotal(cart) + const subtotal = cart.subtotal const requirementResults = option.requirements.map(requirement => { - if (requirement.type === "max_subtotal") { - return requirement.value > subtotal - } else if (requirement.type === "min_subtotal") { - return requirement.value <= subtotal + switch (requirement.type) { + case "max_subtotal": + return requirement.amount > subtotal + case "min_subtotal": + return requirement.amount <= subtotal + default: + return true } - - return true // default to true }) - if (!requirementResults.every(r => r)) { + if (!requirementResults.every(Boolean)) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "The Cart does not satisfy the shipping option's requirements" ) } - option.price = await this.getPrice(option, cart) - return option } @@ -183,39 +306,48 @@ class ShippingOptionService extends BaseService { * @param {ShippingOption} option - the shipping option to create * @return {Promise} the result of the create operation */ - async create(option) { - const region = await this.regionService_.retrieve(option.region_id) + async create(data) { + return this.atomicPhase_(async manager => { + const optionRepo = manager.getCustomRepository(this.optionRepository_) + const option = await optionRepo.create(data) - if (!region.fulfillment_providers.includes(option.provider_id)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The fulfillment provider is not available in the provided region" - ) - } + const region = await this.regionService_.retrieve(option.region_id, { + relations: ["fulfillment_providers"], + }) - const provider = await this.providerService_.retrieveProvider( - option.provider_id - ) - option.price = await this.validatePrice_(option.price, option) + if ( + !region.fulfillment_providers.find( + ({ id }) => id === option.provider_id + ) + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The fulfillment provider is not available in the provided region" + ) + } - const isValid = await provider.validateOption(option.data) - if (!isValid) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The fulfillment provider cannot validate the shipping option" - ) - } + option.price_type = await this.validatePriceType_(data.price_type, option) + option.amount = data.price_type === "calculated" ? null : data.amount - if (option.requirements) { - option.requirements = await Promise.all( - option.requirements.map(r => { - return this.validateRequirement_(r) - }) - ) - } + const isValid = await this.providerService_.validateOption(option) - return this.optionModel_.create(option).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + if (!isValid) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The fulfillment provider cannot validate the shipping option" + ) + } + + if ("requirements" in data) { + option.requirements = await Promise.all( + data.requirements.map(r => { + return this.validateRequirement_(r, option.id) + }) + ) + } + + const result = await optionRepo.save(option) + return result }) } @@ -231,10 +363,10 @@ class ShippingOptionService extends BaseService { * @param {ShippingOption} option - the option to validate against * @return {Promise} the validated price */ - async validatePrice_(price, option) { + async validatePriceType_(priceType, option) { if ( - !price.type || - (price.type !== "flat_rate" && price.type !== "calculated") + !priceType || + (priceType !== "flat_rate" && priceType !== "calculated") ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -242,11 +374,11 @@ class ShippingOptionService extends BaseService { ) } - if (price.type === "calculated") { - const provider = this.providerService_.retrieveProvider( - option.provider_id + if (priceType === "calculated") { + const canCalculate = await this.providerService_.canCalculate( + option.provider_id, + option.data ) - const canCalculate = await provider.canCalculate(option.data) if (!canCalculate) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -255,22 +387,12 @@ class ShippingOptionService extends BaseService { } } - if ( - price.type === "flat_rate" && - (price.amount === undefined || price.amount < 0) - ) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Flat rate prices must be zero or have a postive amount field." - ) - } - - return price + return priceType } /** * Updates a profile. Metadata updates and product updates should use - * dedicated methods, e.g. `setMetadata`, `addProduct`, etc. The function + * dedicated methods, e.g. `setMetadata`, etc. The function * will throw errors if metadata or product updates are attempted. * @param {string} optionId - the id of the option. Must be a string that * can be casted to an ObjectId @@ -278,53 +400,66 @@ class ShippingOptionService extends BaseService { * @return {Promise} resolves to the update result. */ async update(optionId, update) { - const option = await this.retrieve(optionId) - const validatedId = this.validateId_(optionId) + return this.atomicPhase_(async manager => { + const option = await this.retrieve(optionId) - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Use setMetadata to update metadata fields" - ) - } + if ("metadata" in update) { + option.metadata = await this.setMetadata_(option, update.metadata) + } - if (update.region_id || update.provider_id || update.data) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Region and Provider cannot be updated after creation" - ) - } + if (update.region_id || update.provider_id || update.data) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Region and Provider cannot be updated after creation" + ) + } - if (update.requirements) { - update.requirements = update.requirements.reduce((acc, r) => { - const validated = this.validateRequirement_(r) + if ("is_return" in update) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "is_return cannot be changed after creation" + ) + } - if (acc.find(raw => raw.type === validated.type)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Only one requirement of each type is allowed" - ) + if ("requirements" in update) { + const acc = [] + for (const r of update.requirements) { + const validated = await this.validateRequirement_(r, optionId) + + if (acc.find(raw => raw.type === validated.type)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Only one requirement of each type is allowed" + ) + } + + acc.push(validated) } + option.requirements = acc + } - acc.push(validated) + if ("price_type" in update) { + option.price_type = await this.validatePriceType_( + update.price_type, + option + ) + if (update.price_type === "calculated") { + option.amount = null + } + } - return acc - }, []) - } + if ("amount" in update && option.price_type !== "calculated") { + option.amount = update.amount + } - if (update.price) { - update.price = await this.validatePrice_(update.price, option) - } + if ("name" in update) { + option.name = update.name + } - return this.optionModel_ - .updateOne( - { _id: validatedId }, - { $set: update }, - { runValidators: true } - ) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const optionRepo = manager.getCustomRepository(this.optionRepository_) + const result = await optionRepo.save(option) + return result + }) } /** @@ -334,23 +469,24 @@ class ShippingOptionService extends BaseService { * @return {Promise} the result of the delete operation. */ async delete(optionId) { - let option try { - option = await this.retrieve(optionId) + let option = await this.retrieve(optionId) + + const optionRepo = this.manager_.getCustomRepository( + this.optionRepository_ + ) + + return optionRepo.softRemove(option) } catch (error) { // Delete is idempotent, but we return a promise to allow then-chaining return Promise.resolve() } - - return this.optionModel_.deleteOne({ _id: option._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) } /** * @typedef ShippingRequirement * @property {string} type - one of max_subtotal, min_subtotal - * @property {number} value - the value to match against + * @property {number} amount - the value to match against */ /** @@ -361,20 +497,24 @@ class ShippingOptionService extends BaseService { * @return {Promise} the result of update */ async addRequirement(optionId, requirement) { - const option = await this.retrieve(optionId) - const validatedRequirement = this.validateRequirement_(requirement) + return this.atomicPhase_(async manager => { + const option = await this.retrieve(optionId, { + relations: ["requirements"], + }) + const validatedReq = await this.validateRequirement_(requirement) - if (option.requirements.find(r => r.type === validatedRequirement.type)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `A requirement with type: ${validatedRequirement.type} already exists` - ) - } + if (option.requirements.find(r => r.type === validatedReq.type)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `A requirement with type: ${validatedReq.type} already exists` + ) + } - return this.optionModel_.updateOne( - { _id: option._id }, - { $push: { requirements: validatedRequirement } } - ) + option.requirements.push(validatedReq) + + const optionRepo = manager.getCustomRepository(this.optionRepository_) + return optionRepo.save(option) + }) } /** @@ -384,17 +524,24 @@ class ShippingOptionService extends BaseService { * @return {Promise} the result of update */ async removeRequirement(optionId, requirementId) { - const option = await this.retrieve(optionId) + return this.atomicPhase_(async manager => { + const option = await this.retrieve(optionId, { + relations: "requirements", + }) + const newReqs = option.requirements.map(r => { + if (r.id === requirementId) { + return null + } else { + return r + } + }) - if (!option.requirements.find(r => r._id === requirementId)) { - // Remove is idempotent - return Promise.resolve() - } + option.requirements = newReqs.filter(Boolean) - return this.optionModel_.updateOne( - { _id: option._id }, - { $pull: { requirements: { _id: requirementId } } } - ) + const optionRepo = manager.getCustomRepository(this.optionRepository_) + const result = await optionRepo.save(option) + return result + }) } /** @@ -404,12 +551,17 @@ class ShippingOptionService extends BaseService { * @param {string[]} expandFields - fields to expand. * @return {ShippingOption} the decorated ShippingOption. */ - async decorate(shippingOption, fields, expandFields = []) { - const requiredFields = ["_id", "metadata"] - let decorated = _.pick(shippingOption, fields.concat(requiredFields)) + async decorate(optionId, fields = [], expandFields = []) { + const requiredFields = ["id", "metadata"] - const final = await this.runDecorators_(decorated) - return final + fields = fields.concat(requiredFields) + + const option = await this.retrieve(optionId, { + select: fields, + relations: expandFields, + }) + + return option } /** @@ -419,22 +571,26 @@ class ShippingOptionService extends BaseService { * @param {string} value - value for metadata field. * @return {Promise} resolves to the updated result. */ - async setMetadata(optionId, key, value) { - const validatedId = this.validateId_(optionId) + async setMetadata_(option, metadata) { + const existing = option.metadata || {} + const newData = {} + for (const [key, value] of Object.entries(metadata)) { + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) + newData[key] = value } - const keyPath = `metadata.${key}` - return this.optionModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const updated = { + ...existing, + ...newData, + } + + return updated } /** @@ -447,38 +603,11 @@ class ShippingOptionService extends BaseService { * retrieved. * @returns {Promise} the price of the shipping option. */ - async getPrice(option, cart) { - if (option.price && option.price.type === "calculated") { - const provider = this.providerService_.retrieveProvider( - option.provider_id - ) - return provider.calculatePrice(option.data, cart) + async getPrice_(option, data, cart) { + if (option.price_type === "calculated") { + return this.providerService_.calculatePrice(option, data, cart) } - return option.price.amount - } - - /** - * Dedicated method to delete metadata for a shipping option. - * @param {string} optionId - the shipping option to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(optionId, key) { - const validatedId = this.validateId_(optionId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.optionModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + return option.amount } } diff --git a/packages/medusa/src/services/shipping-profile.js b/packages/medusa/src/services/shipping-profile.js index 8a0b226e55..095a5868e8 100644 --- a/packages/medusa/src/services/shipping-profile.js +++ b/packages/medusa/src/services/shipping-profile.js @@ -1,79 +1,102 @@ import _ from "lodash" -import { Validator, MedusaError } from "medusa-core-utils" +import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { Any, In } from "typeorm" /** * Provides layer to manipulate profiles. * @implements BaseService */ class ShippingProfileService extends BaseService { - /** @param { - * shippingProfileModel: (ShippingProfileModel), - * productService: (ProductService), - * shippingOptionService: (ProductService), - * } */ - constructor({ shippingProfileModel, productService, shippingOptionService }) { + constructor({ + manager, + shippingProfileRepository, + productService, + productRepository, + shippingOptionService, + }) { super() - /** @private @const {ShippingProfileModel} */ - this.profileModel_ = shippingProfileModel + /** @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 } - /** - * Used to validate product ids. Throws an error if the cast fails - * @param {string} rawId - the raw product id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The profileId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new ShippingProfileService({ + manager: transactionManager, + shippingProfileRepository: this.shippingProfileRepository_, + productService: this.productService_, + shippingOptionService: this.shippingOptionService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector) { - return this.profileModel_.find(selector) + 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 profiles = await this.list({ products: { $in: productIds } }) + 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 oId => { - const option = await this.shippingOptionService_ - .retrieve(oId) - .catch(_ => undefined) - - if (!option) { - return null - } - + 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 }) ) @@ -87,18 +110,30 @@ class ShippingProfileService extends BaseService { * @param {string} profileId - the id of the profile to get. * @return {Promise} the profile document. */ - async retrieve(profileId) { + async retrieve(profileId, options = {}) { + const profileRepository = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) const validatedId = this.validateId_(profileId) - const profile = await this.profileModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + + 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, - `Shipping Profile with ${profileId} was not found` + `Profile with id: ${profileId} was not found` ) } @@ -106,11 +141,15 @@ class ShippingProfileService extends BaseService { } async retrieveDefault() { - return await this.profileModel_ - .findOne({ name: "default_shipping_profile" }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const profileRepository = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) + + const profile = await profileRepository.findOne({ + where: { type: "default" }, + }) + + return profile } /** @@ -118,12 +157,24 @@ class ShippingProfileService extends BaseService { * @return {Promise} the shipping profile */ async createDefault() { - const profile = await this.retrieveDefault() - if (!profile) { - return this.profileModel_.create({ name: "default_shipping_profile" }) - } + return this.atomicPhase_(async manager => { + let profile = await this.retrieveDefault() - return profile + 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 + }) } /** @@ -131,11 +182,15 @@ class ShippingProfileService extends BaseService { * @return the shipping profile for gift cards */ async retrieveGiftCardDefault() { - return await this.profileModel_ - .findOne({ name: "default_gift_card_profile" }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const profileRepository = this.manager_.getCustomRepository( + this.shippingProfileRepository_ + ) + + const giftCardProfile = await profileRepository.findOne({ + where: { type: "gift_card" }, + }) + + return giftCardProfile } /** @@ -144,12 +199,24 @@ class ShippingProfileService extends BaseService { * @return {Promise} the shipping profile */ async createGiftCardDefault() { - const profile = await this.retrieveGiftCardDefault() - if (!profile) { - return this.profileModel_.create({ name: "default_gift_card_profile" }) - } + return this.atomicPhase_(async manager => { + let profile = await this.retrieveGiftCardDefault() - return profile + 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 + }) } /** @@ -158,13 +225,22 @@ class ShippingProfileService extends BaseService { * @return {Promise} the result of the create operation */ async create(profile) { - if (profile.products || profile.shipping_options) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Please add products and shipping_options after creating Shipping Profiles" + return this.atomicPhase_(async manager => { + const profileRepository = manager.getCustomRepository( + this.shippingProfileRepository_ ) - } - return this.profileModel_.create(profile) + + 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 + }) } /** @@ -177,69 +253,51 @@ class ShippingProfileService extends BaseService { * @return {Promise} resolves to the update result. */ async update(profileId, update) { - const validatedId = this.validateId_(profileId) - - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" + return this.atomicPhase_(async manager => { + const profileRepository = manager.getCustomRepository( + this.shippingProfileRepository_ ) - } - if (update.products) { - // We use the set to ensure that the array doesn't include duplicates - const productSet = new Set(update.products) - - // Go through each product and ensure they exist and if they are found in - // other profiles that they are removed from there. - update.products = await Promise.all( - [...productSet].map(async pId => { - const product = await this.productService_.retrieve(pId) - - // Ensure that every product only exists in exactly one profile - const existing = await this.profileModel_.findOne({ - products: product._id, - }) - if (existing && existing._id !== profileId) { - await this.removeProduct(existing._id, product._id) - } - - return product._id - }) - ) - } - - if (update.shipping_options) { - // No duplicates - const optionSet = new Set(update.shipping_options) - - update.shipping_options = await Promise.all( - [...optionSet].map(async sId => { - const profile = await this.retrieve(profileId) - const shippingOption = await this.shippingOptionService_.retrieve(sId) - - // If the shipping method exists in a different profile remove it - const existing = await this.profileModel_.findOne({ - shipping_options: shippingOption._id, - }) - if (existing && existing._id !== profileId) { - await this.removeShippingOption(existing._id, shippingOption._id) - } - - return shippingOption._id - }) - ) - } - - return this.profileModel_ - .updateOne( - { _id: validatedId }, - { $set: update }, - { runValidators: true } - ) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + 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 + }) } /** @@ -249,16 +307,19 @@ class ShippingProfileService extends BaseService { * @return {Promise} the result of the delete operation. */ async delete(profileId) { - let profile - try { - profile = await this.retrieve(profileId) - } catch (error) { - // Delete is idempotent, but we return a promise to allow then-chaining - return Promise.resolve() - } + return this.atomicPhase_(async manager => { + const profileRepo = manager.getCustomRepository( + this.shippingProfileRepository_ + ) - return this.profileModel_.deleteOne({ _id: profile._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + // 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() }) } @@ -270,19 +331,14 @@ class ShippingProfileService extends BaseService { * @return {Promise} the result of update */ async addProduct(profileId, productId) { - const profile = await this.retrieve(profileId) - const product = await this.productService_.retrieve(productId) + return this.atomicPhase_(async manager => { + await this.productService_ + .withTransaction(manager) + .update(productId, { profile_id: profileId }) - if (profile.products.find(p => p === product._id)) { - // If the product already exists in the profile we just return an - // empty promise for then-chaining - return Promise.resolve() - } - - return this.profileModel_.updateOne( - { _id: profile._id }, - { $push: { products: product._id } } - ) + const updated = await this.retrieve(profileId) + return updated + }) } /** @@ -293,62 +349,14 @@ class ShippingProfileService extends BaseService { * @return {Promise} the result of the model update operation */ async addShippingOption(profileId, optionId) { - const profile = await this.retrieve(profileId) - const shippingOption = await this.shippingOptionService_.retrieve(optionId) + return this.atomicPhase_(async manager => { + await this.shippingOptionService_ + .withTransaction(manager) + .update(optionId, { profile_id: profileId }) - // Make sure that option doesn't already exist - if (profile.shipping_options.find(o => o === shippingOption._id)) { - // If the option already exists in the profile we just return an - // empty promise for then-chaining - return Promise.resolve() - } - - // If the shipping method exists in a different profile remove it - const profiles = await this.list({ shipping_options: shippingOption._id }) - if (profiles.length > 0) { - await this.removeShippingOption(profiles[0]._id, shippingOption._id) - } - - // Everything went well add the shipping option - return this.profileModel_.updateOne( - { _id: profileId }, - { $push: { shipping_options: shippingOption._id } } - ) - } - - /** - * Delete a shipping option from a profile. - * @param {string} profileId - the profile to delete an option from - * @param {string} optionId - the option to delete - * @return {Promise} return the result of update - */ - async removeShippingOption(profileId, optionId) { - const profile = await this.retrieve(profileId) - - return this.profileModel_.updateOne( - { _id: profile._id }, - { $pull: { shipping_options: optionId } } - ) - } - - /** - * Removes a product from the a profile. - * @param {string} profileId - the profile to remove the product from - * @param {string} productId - the product to remove - * @return {Promise} the result of update - */ - async removeProduct(profileId, productId) { - const profile = await this.retrieve(profileId) - - if (!profile.products.find(p => p === productId)) { - // Remove is idempotent - return Promise.resolve() - } - - return this.profileModel_.updateOne( - { _id: profile._id }, - { $pull: { products: productId } } - ) + const updated = await this.retrieve(profileId) + return updated + }) } /** @@ -385,20 +393,12 @@ class ShippingProfileService extends BaseService { * @param {Cart} cart - the cart to extract products from * @return {[string]} a list of product ids */ - getProductsInCart_(cart) { + getProfilesInCart_(cart) { return cart.items.reduce((acc, next) => { - if (Array.isArray(next.content)) { - next.content.forEach(({ product }) => { - if (!acc.includes(product._id)) { - acc.push(product._id) - } - }) - } else { - // We may have line items that are not associated with a product - if (next.content.product) { - if (!acc.includes(next.content.product._id)) { - acc.push(next.content.product._id) - } + // 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) } } @@ -413,82 +413,27 @@ class ShippingProfileService extends BaseService { * @return {[ShippingOptions]} a list of the available shipping options */ async fetchCartOptions(cart) { - const products = this.getProductsInCart_(cart) - const profiles = await this.list({ products: { $in: products } }) - const optionIds = profiles.reduce( - (acc, next) => acc.concat(next.shipping_options), - [] + const profileIds = this.getProfilesInCart_(cart) + + const rawOpts = await this.shippingOptionService_.list( + { + profile_id: profileIds, + }, + { relations: ["requirements", "profile"] } ) - const options = await Promise.all( - optionIds.map(async oId => { - const option = await this.shippingOptionService_ - .validateCartOption(oId, cart) - .catch(_ => { - // If validation failed we skip the option - return null - }) + const options = [] + for (const o of rawOpts) { + try { + const option = this.shippingOptionService_.validateCartOption(o, cart) if (option) { - return { - ...option, - profile: profiles.find(p => p._id.equals(option.profile_id)), - } + options.push(option) } - return null - }) - ) - - return options.filter(o => !!o) - } - - /** - * Dedicated method to set metadata for a profile. - * @param {string} profileId - the profile to decorate. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(profileId, key, value) { - const validatedId = this.validateId_(profileId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) + } catch (error) {} } - const keyPath = `metadata.${key}` - return this.profileModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for a shipping profile. - * @param {string} profileId - the shipping profile to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(profileId, key) { - const validatedId = this.validateId_(profileId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.profileModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + return options } } diff --git a/packages/medusa/src/services/store.js b/packages/medusa/src/services/store.js index f17e294cda..1eed24e6f3 100644 --- a/packages/medusa/src/services/store.js +++ b/packages/medusa/src/services/store.js @@ -1,4 +1,3 @@ -import mongoose from "mongoose" import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" @@ -10,57 +9,84 @@ import { currencies } from "../utils/currencies" * @implements BaseService */ class StoreService extends BaseService { - constructor({ storeModel, eventBusService }) { + constructor({ + manager, + storeRepository, + currencyRepository, + eventBusService, + }) { super() - /** @private @const {storeModel} */ - this.storeModel_ = storeModel + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {StoreRepository} */ + this.storeRepository_ = storeRepository + + /** @private @const {CurrencyRepository} */ + this.currencyRepository_ = currencyRepository /** @private @const {EventBus} */ this.eventBus_ = eventBusService } - /** - * Used to validate customer ids. Throws an error if the cast fails - * @param {string} rawId - the raw customer id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The customerId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new StoreService({ + manager: transactionManager, + storeRepository: this.storeRepository_, + currencyRepository: this.currencyRepository_, + eventBusService: this.eventBus_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** * Creates a store if it doesn't already exist. * @return {Promise} the store. */ - async create(providers) { - let store = await this.retrieve() - if (!store) { - return this.storeModel_.create(providers) - } else { - store = await this.update(providers) - } + async create() { + return this.atomicPhase_(async manager => { + const storeRepository = manager.getCustomRepository(this.storeRepository_) - return store + let store = await this.retrieve() + + if (!store) { + const s = await storeRepository.create() + store = await storeRepository.save(s) + } + + return store + }) } /** * Retrieve the store settings. There is always a maximum of one store. - * @return {Promise} the customer document. + * @return {Promise} the store */ - retrieve() { - return this.storeModel_.findOne().catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieve(relations = []) { + const storeRepo = this.manager_.getCustomRepository(this.storeRepository_) + + const store = await storeRepo.findOne({ relations }) + + return store + } + + getDefaultCurrency_(code) { + const currencyObject = currencies[code.toUpperCase()] + + return { + code: currencyObject.code.toLowerCase(), + symbol: currencyObject.symbol, + symbol_native: currencyObject.symbol_native, + name: currencyObject.name, + } } /** @@ -73,42 +99,68 @@ class StoreService extends BaseService { * @return {Promise} resolves to the update result. */ async update(update) { - const store = await this.retrieve() - - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" + return this.atomicPhase_(async manager => { + const storeRepository = manager.getCustomRepository(this.storeRepository_) + const currencyRepository = manager.getCustomRepository( + this.currencyRepository_ ) - } - if (update.default_currency) { - update.default_currency = update.default_currency.toUpperCase() - if (!currencies[update.default_currency]) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Invalid currency ${update.default_currency}` - ) + const store = await this.retrieve() + + const { + metadata, + default_currency, + default_currency_code, + currencies: storeCurrencies, + ...rest + } = update + + if (metadata) { + store.metadata = this.setMetadata_(store.id, metadata) } - } - if (update.currencies) { - update.currencies = update.currencies.map(c => c.toUpperCase()) - update.currencies.forEach(c => { - if (!currencies[c]) { + if (default_currency_code) { + const curr = await currencyRepository.findOne({ + code: default_currency_code.toLowerCase(), + }) + + if (!curr) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `Invalid currency ${c}` + `Currency ${default_currency_code} not found` ) } - }) - } - return this.storeModel_ - .updateOne({ _id: store._id }, { $set: update }, { runValidators: true }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + store.default_currency = curr + store.default_currency_code = curr.code + } + + if (storeCurrencies) { + store.currencies = await Promise.all( + storeCurrencies.map(async curr => { + const currency = await currencyRepository.findOne({ + where: { code: curr.toLowerCase() }, + }) + + if (!currency) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Invalid currency ${curr}` + ) + } + + return currency + }) + ) + } + + for (const [key, value] of Object.entries(rest)) { + store[key] = value + } + + const result = await storeRepository.save(store) + return result + }) } /** @@ -117,29 +169,35 @@ class StoreService extends BaseService { * @return {Promise} result after update */ async addCurrency(code) { - code = code.toUpperCase() - const store = await this.retrieve() - - if (!currencies[code]) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Invalid currency ${code}` + return this.atomicPhase_(async manager => { + const storeRepo = manager.getCustomRepository(this.storeRepository_) + const currencyRepository = manager.getCustomRepository( + this.currencyRepository_ ) - } + const store = await this.retrieve(["currencies"]) - if (store.currencies.includes(code)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Currency already added` - ) - } + const curr = await currencyRepository.findOne({ + where: { code: code.toLowerCase() }, + }) - return this.storeModel_.updateOne( - { - _id: store._id, - }, - { $push: { currencies: code } } - ) + if (!curr) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Currency ${code} not found` + ) + } + + if (store.currencies.map(c => c.code).includes(curr.code.toLowerCase())) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Currency already added` + ) + } + + store.currencies = [...store.currencies, curr] + const updated = await storeRepo.save(store) + return updated + }) } /** @@ -148,14 +206,20 @@ class StoreService extends BaseService { * @return {Promise} result after update */ async removeCurrency(code) { - const store = await this.retrieve() - code = code.toUpperCase() - return this.storeModel_.updateOne( - { - _id: store._id, - }, - { $pull: { currencies: code } } - ) + return this.atomicPhase_(async manager => { + const storeRepo = manager.getCustomRepository(this.storeRepository_) + const store = await this.retrieve(["currencies"]) + + const exists = store.currencies.find(c => c.code === code.toLowerCase()) + // If currency does not exist, return early + if (!exists) { + return store + } + + store.currencies = store.currencies.filter(c => c.code !== code) + const updated = await storeRepo.save(store) + return updated + }) } /** @@ -168,32 +232,6 @@ class StoreService extends BaseService { async decorate(store, fields, expandFields = []) { return store } - - /** - * Dedicated method to set metadata for a store. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} customerId - the customer to apply metadata to. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(key, value) { - const store = await this.retrieve() - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.storeModel_ - .updateOne({ _id: store._id }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } } export default StoreService diff --git a/packages/medusa/src/services/swap.js b/packages/medusa/src/services/swap.js index 6f0b051a94..9393052c55 100644 --- a/packages/medusa/src/services/swap.js +++ b/packages/medusa/src/services/swap.js @@ -1,6 +1,6 @@ import _ from "lodash" import { BaseService } from "medusa-interfaces" -import { Validator, MedusaError } from "medusa-core-utils" +import { MedusaError } from "medusa-core-utils" /** * Handles swaps @@ -17,7 +17,8 @@ class SwapService extends BaseService { } constructor({ - swapModel, + manager, + swapRepository, eventBusService, cartService, totalsService, @@ -29,8 +30,11 @@ class SwapService extends BaseService { }) { super() + /** @private @const {EntityManager} */ + this.manager_ = manager + /** @private @const {SwapModel} */ - this.swapModel_ = swapModel + this.swapRepository_ = swapRepository /** @private @const {TotalsService} */ this.totalsService_ = totalsService @@ -57,32 +61,27 @@ class SwapService extends BaseService { this.eventBus_ = eventBusService } - /** - * @param {Object} selector - the query object for find - * @return {Promise} the result of the find operation - */ - list(selector, offset, limit) { - return this.swapModel_ - .find(selector, {}, offset, limit) - .sort({ created: -1 }) - } - - /** - * Used to validate user ids. Throws an error if the cast fails - * @param {string} rawId - the raw user id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The swapId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new SwapService({ + manager: transactionManager, + swapRepository: this.swapRepository_, + eventBusService: this.eventBus_, + cartService: this.cartService_, + totalsService: this.totalsService_, + returnService: this.returnService_, + lineItemService: this.lineItemService_, + paymentProviderService: this.paymentProviderService_, + shippingOptionService: this.shippingOptionService_, + fulfillmentService: this.fulfillmentService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -90,10 +89,14 @@ class SwapService extends BaseService { * @param {string} id - the id of the swap to retrieve * @return {Promise} the swap */ - async retrieve(id) { - const validatedId = this.validateId_(id) - const swap = await this.swapModel_.findOne({ _id: validatedId }) + async retrieve(id, config = {}) { + const swapRepo = this.manager_.getCustomRepository(this.swapRepository_) + const validatedId = this.validateId_(id) + + const query = this.buildQuery_({ id: validatedId }, config) + + const swap = await swapRepo.findOne(query) if (!swap) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Swap was not found") } @@ -106,8 +109,15 @@ class SwapService extends BaseService { * @param {string} cartId - the cart id that the swap's cart has * @return {Promise} the swap */ - async retrieveByCartId(cartId) { - const swap = await this.swapModel_.findOne({ cart_id: cartId }) + async retrieveByCartId(cartId, relations = []) { + const swapRepo = this.manager_.getCustomRepository(this.swapRepository_) + + const swap = await swapRepo.findOne({ + where: { + cart_id: cartId, + }, + relations, + }) if (!swap) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Swap was not found") @@ -116,6 +126,19 @@ class SwapService extends BaseService { return swap } + /** + * @param {Object} selector - the query object for find + * @return {Promise} the result of the find operation + */ + list( + selector, + config = { skip: 0, take: 50, order: { created_at: "DESC" } } + ) { + const swapRepo = this.manager_.getCustomRepository(this.swapRepository_) + const query = this.buildQuery_(selector, config) + return swapRepo.find(query) + } + /** * @typedef OrderLike * @property {Array} items - the items on the order @@ -137,7 +160,7 @@ class SwapService extends BaseService { */ validateReturnItems_(order, returnItems) { return returnItems.map(({ item_id, quantity }) => { - const item = order.items.find(i => i._id.equals(item_id)) + const item = order.items.find(i => i.id === item_id) // The item must exist in the order if (!item) { @@ -176,152 +199,147 @@ class SwapService extends BaseService { * returning the returnItems. * @returns {Promise} the newly created swap. */ - async create(order, returnItems, additionalItems, returnShipping) { - if ( - order.fulfillment_status === "not_fulfilled" || - order.payment_status !== "captured" - ) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Order cannot be swapped" - ) - } - - const newItems = await Promise.all( - additionalItems.map(({ variant_id, quantity }) => { - return this.lineItemService_.generate( - variant_id, - order.region_id, - quantity + async create( + order, + returnItems, + additionalItems, + returnShipping, + custom = {} + ) { + return this.atomicPhase_(async manager => { + if ( + order.fulfillment_status === "not_fulfilled" || + order.payment_status !== "captured" + ) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Order cannot be swapped" ) + } + + const newItems = await Promise.all( + additionalItems.map(({ variant_id, quantity }) => { + return this.lineItemService_.generate( + variant_id, + order.region_id, + quantity + ) + }) + ) + + const swapRepo = manager.getCustomRepository(this.swapRepository_) + const created = swapRepo.create({ + ...custom, + fulfillment_status: "not_fulfilled", + payment_status: "not_paid", + order_id: order.id, + additional_items: newItems, }) - ) - const validatedReturnItems = this.validateReturnItems_(order, returnItems) + const result = await swapRepo.save(created) - return this.swapModel_.create({ - order_id: order._id, - order_payment: order.payment_method, - region_id: order.region_id, - currency_code: order.currency_code, - return_items: validatedReturnItems, - return_shipping: returnShipping, - additional_items: newItems, + await this.returnService_.withTransaction(manager).create( + { + swap_id: result.id, + items: returnItems, + shipping_method: returnShipping, + }, + order + ) + + return result }) } async processDifference(swapId) { - const swap = await this.retrieve(swapId) + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId, { + relations: ["payment", "order", "order.payments"], + }) - if (!swap.is_paid) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Cannot process a swap that hasn't been confirmed by the customer" - ) - } - - if (swap.amount_paid < 0) { - const { provider_id, data } = swap.order_payment - const paymentProvider = this.paymentProviderService_.retrieveProvider( - provider_id - ) - - try { - await paymentProvider.refundPayment(data, -1 * swap.amount_paid) - } catch (err) { - return this.swapModel_ - .updateOne( - { - _id: swapId, - }, - { - $set: { payment_status: "requires_action" }, - } - ) - .then(result => { - this.eventBus_.emit( - SwapService.Events.PROCESS_REFUND_FAILED, - result - ) - return result - }) + if (!swap.confirmed_at) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Cannot process a swap that hasn't been confirmed by the customer" + ) } - return this.swapModel_ - .updateOne( - { - _id: swapId, - }, - { - $set: { payment_status: "difference_refunded" }, - } - ) - .then(result => { - this.eventBus_.emit(SwapService.Events.REFUND_PROCESSED, result) + const swapRepo = manager.getCustomRepository(this.swapRepository_) + if (swap.difference_due < 0) { + try { + await this.paymentProviderService_ + .withTransaction(manager) + .refundPayment( + swap.order.payments, + -1 * swap.difference_due, + "swap" + ) + } catch (err) { + swap.payment_status = "requires_action" + const result = await swapRepo.save(swap) + await this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.PROCESS_REFUND_FAILED, result) return result - }) - } else if (swap.amount_paid === 0) { - return this.swapModel_.updateOne( - { - _id: swapId, - }, - { - $set: { payment_status: "difference_refunded" }, } - ) - } - return this.capturePayment(swapId) + swap.payment_status = "difference_refunded" + + const result = await swapRepo.save(swap) + + await this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.REFUND_PROCESSED, result) + return result + } else if (swap.difference_due === 0) { + swap.payment_status = "difference_refunded" + + const result = await swapRepo.save(swap) + await this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.REFUND_PROCESSED, result) + return result + } + + try { + await this.paymentProviderService_ + .withTransaction(manager) + .capturePayment(swap.payment) + } catch (err) { + swap.payment_status = "requires_action" + const result = await swapRepo.save(swap) + await this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.PAYMENT_CAPTURE_FAILED, result) + return result + } + + swap.payment_status = "captured" + + const result = await swapRepo.save(swap) + await this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.PAYMENT_CAPTURED, result) + return result + }) } - async capturePayment(swapId) { - const swap = await this.retrieve(swapId) + async update(swapId, update) { + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId) - if (swap.payment_status !== "awaiting") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Payment already captured" - ) - } + if ("metadata" in update) { + swap.metadata = this.setMetadata_(swap, update.metadata) + } - const updateFields = { payment_status: "captured" } + if ("shipping_address" in update) { + await this.updateShippingAddress_(swap, update.shipping_address) + } - const { provider_id, data } = swap.payment_method - const paymentProvider = await this.paymentProviderService_.retrieveProvider( - provider_id - ) - - try { - await paymentProvider.capturePayment(data) - } catch (error) { - return this.swapModel_ - .updateOne( - { - _id: swapId, - }, - { - $set: { payment_status: "requires_action" }, - } - ) - .then(result => { - this.eventBus_.emit(SwapService.Events.PAYMENT_CAPTURE_FAILED, result) - return result - }) - } - - return this.swapModel_ - .updateOne( - { - _id: swapId, - }, - { - $set: updateFields, - } - ) - .then(result => { - this.eventBus_.emit(SwapService.Events.PAYMENT_CAPTURED, result) - return result - }) + const swapRepo = manager.getCustomRepository(this.swapRepository_) + const result = await swapRepo.save(swap) + return result + }) } /** @@ -334,196 +352,175 @@ class SwapService extends BaseService { * @returns {Promise} the swap with its cart_id prop set to the id of * the new cart. */ - async createCart(order, swapId) { - const swap = await this.retrieve(swapId) + async createCart(swapId) { + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId, { + relations: [ + "order", + "order.items", + "order.discounts", + "additional_items", + "return_order", + "return_order.items", + "return_order.shipping_method", + ], + }) - if (!order._id.equals(swap.order_id)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The swap does not belong to the order" - ) - } - - if (swap.cart_id) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "A cart has already been created for the swap" - ) - } - - // Add return lines to the cart to ensure that the total calculation is - // correct. - const returnLines = swap.return_items.map(r => { - const lineItem = order.items.find(i => i._id.equals(r.item_id)) - - return { - ...lineItem, - content: { - ...lineItem.content, - unit_price: -1 * lineItem.content.unit_price, - }, - quantity: r.quantity, - metadata: { - ...lineItem.metadata, - is_return_line: true, - }, + if (swap.cart_id) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "A cart has already been created for the swap" + ) } - }) - // If the swap has a return shipping method the price has to be added to the - // cart. - if (swap.return_shipping) { - returnLines.push({ - title: "Return shipping", - quantity: 1, - has_shipping: true, - no_discount: true, - content: { - unit_price: swap.return_shipping.price, - quantity: 1, - }, + const order = swap.order + + const cart = await this.cartService_.withTransaction(manager).create({ + discounts: order.discounts, + email: order.email, + billing_address_id: order.billing_address_id, + shipping_address_id: order.shipping_address_id, + region_id: order.region_id, + customer_id: order.customer_id, + type: "swap", metadata: { - is_return_line: true, + swap_id: swap.id, + parent_order_id: order.id, }, }) - } - const cart = await this.cartService_.create({ - discounts: order.discounts, - email: order.email, - billing_address: order.billing_address, - shipping_address: order.shipping_address, - items: [...returnLines, ...swap.additional_items], - region_id: order.region_id, - customer_id: order.customer_id, - is_swap: true, - metadata: { - swap_id: swap._id, - parent_order_id: order._id, - }, + for (const item of swap.additional_items) { + await this.lineItemService_.withTransaction(manager).update(item.id, { + cart_id: cart.id, + }) + } + + // If the swap has a return shipping method the price has to be added to the + // cart. + if (swap.return_order && swap.return_order.shipping_method) { + await this.lineItemService_.withTransaction(manager).create({ + cart_id: cart.id, + title: "Return shipping", + quantity: 1, + has_shipping: true, + allow_discounts: false, + unit_price: swap.return_order.shipping_method.price, + metadata: { + is_return_line: true, + }, + }) + } + + for (const r of swap.return_order.items) { + const lineItem = order.items.find(i => i.id === r.item_id) + + const toCreate = { + cart_id: cart.id, + thumbnail: lineItem.thumbnail, + title: lineItem.title, + variant_id: lineItem.variant_id, + unit_price: -1 * lineItem.unit_price, + quantity: r.quantity, + metadata: { + ...lineItem.metadata, + is_return_line: true, + }, + } + + await this.lineItemService_.withTransaction(manager).create(toCreate) + } + + swap.cart_id = cart.id + + const swapRepo = manager.getCustomRepository(this.swapRepository_) + const result = await swapRepo.save(swap) + return result }) - - return this.swapModel_.updateOne( - { _id: swapId }, - { $set: { cart_id: cart._id } } - ) } /** * */ - async registerCartCompletion(swapId, cartId) { - const swap = await this.retrieve(swapId) - const cart = await this.cartService_.retrieve(cartId) - - // If we already registered the cart completion we just return - if (swap.is_paid) { - return swap - } - - if (!cart._id.equals(swap.cart_id)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Cart does not belong to swap" - ) - } - - const total = await this.totalsService_.getTotal(cart) - - let payment = {} - - if (total > 0) { - let paymentSession = {} - let paymentData = {} - const { payment_method, payment_sessions } = cart - - if (!payment_method) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Cart does not contain a payment method" - ) - } - - if (!payment_sessions || !payment_sessions.length) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "cart must have payment sessions" - ) - } - - paymentSession = payment_sessions.find( - ps => ps.provider_id === payment_method.provider_id - ) - - // Throw if payment method does not exist - if (!paymentSession) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Cart does not have an authorized payment session" - ) - } - - const paymentProvider = this.paymentProviderService_.retrieveProvider( - paymentSession.provider_id - ) - const paymentStatus = await paymentProvider.getStatus(paymentSession.data) - - // If payment status is not authorized, we throw - if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Payment method is not authorized" - ) - } - - paymentData = await paymentProvider.retrievePayment(paymentSession.data) - - if (paymentSession.provider_id) { - payment = { - provider_id: paymentSession.provider_id, - data: paymentData, - } - } - } - - return this.swapModel_ - .updateOne( - { _id: swap._id }, - { - shipping_address: cart.shipping_address, - shipping_methods: cart.shipping_methods, - is_paid: true, - amount_paid: total, - payment_method: payment, - } - ) - .then(result => { - this.eventBus_.emit(SwapService.Events.PAYMENT_COMPLETED, { - swap: result, - }) - return result + async registerCartCompletion(swapId) { + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId, { + relations: [ + "cart", + "cart.region", + "cart.shipping_methods", + "cart.shipping_address", + "cart.items", + "cart.discounts", + "cart.discounts.rule", + "cart.payment", + "cart.gift_cards", + ], }) - } - /** - * Requests a return based off an order and a swap. - * @param {Order} order - the order to create the return from. - * @param {string} swapId - the id to create the return from - * @returns {Promise} the swap - */ - async requestReturn(order, swapId) { - const swap = await this.retrieve(swapId) + // If we already registered the cart completion we just return + if (swap.confirmed_at) { + return swap + } - const newReturn = await this.returnService_.requestReturn( - order, - swap.return_items, - swap.return_shipping - ) + const cart = swap.cart - return this.swapModel_.updateOne( - { _id: swapId }, - { $set: { return: newReturn } } - ) + const total = await this.totalsService_.getTotal(cart) + + if (total > 0) { + const { payment } = cart + + if (!payment) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Cart does not contain a payment" + ) + } + + const paymentStatus = await this.paymentProviderService_ + .withTransaction(manager) + .getStatus(payment) + + // If payment status is not authorized, we throw + if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Payment method is not authorized" + ) + } + + await this.paymentProviderService_ + .withTransaction(manager) + .updatePayment(payment.id, { + swap_id: swapId, + }) + } + + const now = new Date() + swap.difference_due = total + swap.shipping_address_id = cart.shipping_address_id + swap.shipping_methods = cart.shipping_methods + swap.confirmed_at = now.toISOString() + swap.payment_status = total === 0 ? "difference_refunded" : "awaiting" + + const swapRepo = manager.getCustomRepository(this.swapRepository_) + const result = await swapRepo.save(swap) + + for (const method of cart.shipping_methods) { + await this.shippingOptionService_ + .withTransaction(manager) + .updateShippingMethod(method.id, { + swap_id: result.id, + }) + } + + this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.PAYMENT_COMPLETED, { + id: swap.id, + }) + + return result + }) } /** @@ -536,48 +533,31 @@ class SwapService extends BaseService { * @returns {Promise} the resulting swap, with an updated return and * status. */ - async receiveReturn(order, swapId, returnItems) { - const swap = await this.retrieve(swapId) + async receiveReturn(swapId, returnItems) { + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId, { relations: ["return_order"] }) - const returnRequest = swap.return - if (!returnRequest || !returnRequest._id) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Swap has no return request" - ) - } - - if (!order._id.equals(swap.order_id)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The swap does not belong to the order" - ) - } - - const updatedReturn = await this.returnService_.receiveReturn( - order, - returnRequest, - returnItems, - returnRequest.refund_amount, - false - ) - - let status = "received" - if (updatedReturn.status === "requires_action") { - status = "requires_action" - } - - return this.swapModel_.updateOne( - { - _id: swapId, - }, - { - $set: { - status, - return: updatedReturn, - }, + const returnId = swap.return_order && swap.return_order.id + if (!returnId) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Swap has no return request" + ) } - ) + + const updatedRet = await this.returnService_ + .withTransaction(manager) + .receiveReturn(returnId, returnItems, undefined, false) + + if (updatedRet.status === "requires_action") { + const swapRepo = manager.getCustomRepository(this.swapRepository_) + swap.fulfillment_status = "requires_action" + const result = await swapRepo.save(swap) + return result + } + + return this.retrieve(swapId) + }) } /** @@ -587,40 +567,96 @@ class SwapService extends BaseService { * @param {object} metadata - optional metadata to attach to the fulfillment. * @returns {Promise} the updated swap with new status and fulfillments. */ - async createFulfillment(order, swapId, metadata = {}) { - const swap = await this.retrieve(swapId) + async createFulfillment(swapId, metadata = {}) { + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId, { + relations: [ + "payment", + "shipping_address", + "additional_items", + "shipping_methods", + "order", + "order.billing_address", + "order.discounts", + "order.payments", + ], + }) + const order = swap.order - const fulfillments = await this.fulfillmentService_.createFulfillment( - { - ...swap, - email: order.email, - currency_code: order.currency_code, - tax_rate: order.tax_rate, - region_id: order.region_id, - display_id: order.display_id, - billing_address: order.billing_address, - items: swap.additional_items, - shipping_methods: swap.shipping_methods, - is_swap: true, - }, - swap.additional_items.map(i => ({ - item_id: i._id, - quantity: i.quantity, - })), - metadata - ) - - return this.swapModel_.updateOne( - { - _id: swapId, - }, - { - $set: { - fulfillment_status: "fulfilled", - fulfillments, - }, + if (swap.fulfillment_status !== "not_fulfilled") { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "The swap was already fulfilled" + ) } - ) + + if (!swap.shipping_methods?.length) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Cannot fulfill an swap that doesn't have shipping methods" + ) + } + + swap.fulfillments = await this.fulfillmentService_ + .withTransaction(manager) + .createFulfillment( + { + ...swap, + payments: swap.payment ? [swap.payment] : order.payments, + email: order.email, + discounts: order.discounts, + currency_code: order.currency_code, + tax_rate: order.tax_rate, + region_id: order.region_id, + display_id: order.display_id, + billing_address: order.billing_address, + items: swap.additional_items, + shipping_methods: swap.shipping_methods, + is_swap: true, + }, + swap.additional_items.map(i => ({ + item_id: i.id, + quantity: i.quantity, + })), + { swap_id: swapId, metadata } + ) + + let successfullyFulfilled = [] + for (const f of swap.fulfillments) { + successfullyFulfilled = successfullyFulfilled.concat(f.items) + } + + swap.fulfillment_status = "fulfilled" + + // Update all line items to reflect fulfillment + for (const item of swap.additional_items) { + const fulfillmentItem = successfullyFulfilled.find( + f => item.id === f.item_id + ) + + if (fulfillmentItem) { + const fulfilledQuantity = + (item.fulfilled_quantity || 0) + fulfillmentItem.quantity + + // Update the fulfilled quantity + await this.lineItemService_.withTransaction(manager).update(item.id, { + fulfilled_quantity: fulfilledQuantity, + }) + + if (item.quantity !== fulfilledQuantity) { + swap.fulfillment_status = "requires_action" + } + } else { + if (item.quantity !== item.fulfilled_quantity) { + swap.fulfillment_status = "requires_action" + } + } + } + + const swapRepo = manager.getCustomRepository(this.swapRepository_) + const result = await swapRepo.save(swap) + return result + }) } /** @@ -634,98 +670,78 @@ class SwapService extends BaseService { * @returns {Promise} the updated swap with new fulfillments and status. */ async createShipment(swapId, fulfillmentId, trackingNumbers, metadata = {}) { - const swap = await this.retrieve(swapId) - - // Update the fulfillment to register - const updatedFulfillments = await Promise.all( - swap.fulfillments.map(f => { - if (f._id.equals(fulfillmentId)) { - return this.fulfillmentService_.createShipment( - { - items: swap.additional_items, - shipping_methods: swap.shipping_methods, - }, - f, - trackingNumbers, - metadata - ) - } - return f + return this.atomicPhase_(async manager => { + const swap = await this.retrieve(swapId, { + relations: ["additional_items"], }) - ) - // Go through all the additional items in the swap - const updatedItems = swap.additional_items.map(i => { - let shipmentItem - for (const fulfillment of updatedFulfillments) { - const item = fulfillment.items.find(fi => i._id.equals(fi._id)) - if (!!item) { - shipmentItem = item - break + // Update the fulfillment to register + const shipment = await this.fulfillmentService_ + .withTransaction(manager) + .createShipment(fulfillmentId, trackingNumbers, metadata) + + swap.fulfillment_status = "shipped" + + // 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, { + shipped_quantity: shippedQty, + }) + + if (shippedQty !== i.quantity) { + swap.fulfillment_status = "partially_shipped" + } + } else { + if (i.shipped_quantity !== i.quantity) { + swap.fulfillment_status = "partially_shipped" + } } } - if (shipmentItem) { - const shippedQuantity = i.shipped_quantity + shipmentItem.quantity - return { - ...i, - shipped: i.quantity === shippedQuantity, - shipped_quantity: shippedQuantity, - } - } - - return i - }) - - const fulfillment_status = updatedItems.every(i => i.shipped) - ? "shipped" - : "partially_shipped" - - return this.swapModel_ - .updateOne( - { _id: swapId }, - { - $set: { - fulfillment_status, - additional_items: updatedItems, - fulfillments: updatedFulfillments, - }, - } - ) - .then(result => { - this.eventBus_.emit(SwapService.Events.SHIPMENT_CREATED, { - swap_id: swapId, - shipment: result.fulfillments.find(f => f._id.equals(fulfillmentId)), + const swapRepo = manager.getCustomRepository(this.swapRepository_) + const result = await swapRepo.save(swap) + await this.eventBus_ + .withTransaction(manager) + .emit(SwapService.Events.SHIPMENT_CREATED, { + id: swapId, + fulfillment_id: shipment.id, }) - return result - }) + return result + }) } /** - * Dedicated method to set metadata for a swap. + * Dedicated method to set metadata for an order. * To ensure that plugins does not overwrite each * others metadata fields, setMetadata is provided. - * @param {string} swapId - the swap to decorate. + * @param {string} orderId - the order to decorate. * @param {string} key - key for metadata field * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. + * @return {Promise} resolves to the updated result. */ - async setMetadata(swapId, key, value) { - const validatedId = this.validateId_(swapId) + setMetadata_(swap, metadata) { + const existing = swap.metadata || {} + const newData = {} + for (const [key, value] of Object.entries(metadata)) { + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) + newData[key] = value } - const keyPath = `metadata.${key}` - return this.swapModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const updated = { + ...existing, + ...newData, + } + + return updated } /** diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index c6a9a09fac..d907b5c2a2 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -7,27 +7,23 @@ import { MedusaError } from "medusa-core-utils" * @implements BaseService */ class TotalsService extends BaseService { - constructor({ productVariantService, regionService }) { + constructor() { super() - /** @private @const {ProductVariantService} */ - this.productVariantService_ = productVariantService - - /** @private @const {RegionService} */ - this.regionService_ = regionService } /** * Calculates subtotal of a given cart or order. - * @param {Cart || Order} object - cart or order to calculate subtotal for + * @param {object} object - object to calculate total for * @return {int} the calculated subtotal */ - async getTotal(object) { - const subtotal = await this.getSubtotal(object) - const taxTotal = await this.getTaxTotal(object) - const discountTotal = await this.getDiscountTotal(object) - const shippingTotal = await this.getShippingTotal(object) + getTotal(object) { + const subtotal = this.getSubtotal(object) + const taxTotal = this.getTaxTotal(object) + const discountTotal = this.getDiscountTotal(object) + const giftCardTotal = this.getGiftCardTotal(object) + const shippingTotal = this.getShippingTotal(object) - return subtotal + taxTotal + shippingTotal - discountTotal + return subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal } /** @@ -42,21 +38,15 @@ class TotalsService extends BaseService { } object.items.map(item => { - if (Array.isArray(item.content)) { - const temp = _.sumBy(item.content, c => c.unit_price * c.quantity) - subtotal += temp * item.quantity - } else { - if (opts.excludeNonDiscounts) { - if (!item.no_discount) { - subtotal += - item.content.unit_price * item.content.quantity * item.quantity - } - } else { - subtotal += - item.content.unit_price * item.content.quantity * item.quantity + if (opts.excludeNonDiscounts) { + if (item.allow_discounts) { + subtotal += item.unit_price * item.quantity } + } else { + subtotal += item.unit_price * item.quantity } }) + return this.rounded(subtotal) } @@ -65,8 +55,8 @@ class TotalsService extends BaseService { * @param {Cart | Object} object - cart or order to calculate subtotal for * @return {int} shipping total */ - getShippingTotal(order) { - const { shipping_methods } = order + getShippingTotal(object) { + const { shipping_methods } = object return shipping_methods.reduce((acc, next) => { return acc + next.price }, 0) @@ -78,43 +68,48 @@ class TotalsService extends BaseService { * @param {Cart | Object} object - cart or order to calculate subtotal for * @return {int} tax total */ - async getTaxTotal(object) { + getTaxTotal(object) { const subtotal = this.getSubtotal(object) const shippingTotal = this.getShippingTotal(object) - const discountTotal = await this.getDiscountTotal(object) - const region = await this.regionService_.retrieve(object.region_id) - const { tax_rate } = region - return this.rounded((subtotal - discountTotal + shippingTotal) * tax_rate) + const discountTotal = this.getDiscountTotal(object) + const giftCardTotal = this.getGiftCardTotal(object) + const tax_rate = object.tax_rate || object.region.tax_rate + return this.rounded( + (subtotal - discountTotal - giftCardTotal + shippingTotal) * + (tax_rate / 100) + ) } getRefundedTotal(object) { + if (!object.refunds) { + return 0 + } + const total = object.refunds.reduce((acc, next) => acc + next.amount, 0) return this.rounded(total) } - getLineItemRefund(order, lineItem) { - const { tax_rate, discounts } = order - const taxRate = tax_rate || 0 + getLineItemRefund(object, lineItem) { + const { discounts } = object + const tax_rate = object.tax_rate || object.region.tax_rate + const taxRate = (tax_rate || 0) / 100 - const discount = discounts.find( - ({ discount_rule }) => discount_rule.type !== "free_shipping" - ) + const discount = discounts.find(({ rule }) => rule.type !== "free_shipping") if (!discount) { - return lineItem.content.unit_price * lineItem.quantity * (1 + taxRate) + return lineItem.unit_price * lineItem.quantity * (1 + taxRate) } - const lineDiscounts = this.getLineDiscounts(order, discount) - const discountedLine = lineDiscounts.find(line => - line.item._id.equals(lineItem._id) + const lineDiscounts = this.getLineDiscounts(object, discount) + const discountedLine = lineDiscounts.find( + line => line.item.id === lineItem.id ) const discountAmount = (discountedLine.amount / discountedLine.item.quantity) * lineItem.quantity return this.rounded( - (lineItem.content.unit_price * lineItem.quantity - discountAmount) * - (1 + taxRate) + (lineItem.unit_price * lineItem.quantity - discountAmount) * (1 + taxRate) ) } @@ -127,7 +122,17 @@ class TotalsService extends BaseService { * @return {int} the calculated subtotal */ getRefundTotal(order, lineItems) { - const refunds = lineItems.map(i => this.getLineItemRefund(order, i)) + const itemIds = order.items.map(i => i.id) + const refunds = lineItems.map(i => { + if (!itemIds.includes(i.id)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Line item does not exist on order" + ) + } + + return this.getLineItemRefund(order, i) + }) return this.rounded(refunds.reduce((acc, next) => acc + next, 0)) } @@ -142,7 +147,7 @@ class TotalsService extends BaseService { * applied discount */ calculateDiscount_(lineItem, variant, variantPrice, value, discountType) { - if (lineItem.no_discount) { + if (!lineItem.allow_discounts) { return { lineItem, variant, @@ -168,7 +173,7 @@ class TotalsService extends BaseService { } /** - * If the discount_rule of a discount has allocation="item", then we need + * If the rule of a discount has allocation="item", then we need * to calculate discount on each item in the cart. Furthermore, we need to * make sure to only apply the discount on valid variants. And finally we * return ether an array of percentages discounts or fixed discounts @@ -181,23 +186,18 @@ class TotalsService extends BaseService { getAllocationItemDiscounts(discount, cart) { const discounts = [] for (const item of cart.items) { - if (discount.discount_rule.valid_for.length > 0) { - discount.discount_rule.valid_for.map(variant => { - // Discounts do not apply to bundles, hence: - if (Array.isArray(item.content)) { - return discounts - } else { - if (item.content.variant._id === variant) { - discounts.push( - this.calculateDiscount_( - item, - variant, - item.content.unit_price, - discount.discount_rule.value, - discount.discount_rule.type - ) + if (discount.rule.valid_for.length > 0) { + discount.rule.valid_for.map(({ id }) => { + if (item.variant.product_id === id) { + discounts.push( + this.calculateDiscount_( + item, + item.variant.id, + item.unit_price, + discount.rule.value, + discount.rule.type ) - } + ) } }) } @@ -206,8 +206,8 @@ class TotalsService extends BaseService { } getLineDiscounts(cart, discount) { - const subtotal = this.getSubtotal(cart) - const { type, allocation, value } = discount.discount_rule + const subtotal = this.getSubtotal(cart, { excludeNonDiscounts: true }) + const { type, allocation, value } = discount.rule if (allocation === "total") { let percentage = 0 if (type === "percentage") { @@ -220,7 +220,7 @@ class TotalsService extends BaseService { } return cart.items.map(item => { - const lineTotal = item.content.unit_price * item.quantity + const lineTotal = item.unit_price * item.quantity return { item, @@ -234,8 +234,8 @@ class TotalsService extends BaseService { type ) return cart.items.map(item => { - const discounted = allocationDiscounts.find(a => - a.lineItem._id.equals(item._id) + const discounted = allocationDiscounts.find( + a => a.lineItem.id === item.id ) return { item, @@ -247,46 +247,51 @@ class TotalsService extends BaseService { return cart.items.map(i => ({ item: i, amount: 0 })) } + getGiftCardTotal(cart) { + const giftCardable = this.getSubtotal(cart) - this.getDiscountTotal(cart) + + if (cart.gift_card_transactions) { + return cart.gift_card_transactions.reduce( + (acc, next) => acc + next.amount, + 0 + ) + } + + if (!cart.gift_cards || !cart.gift_cards.length) { + return 0 + } + + const toReturn = cart.gift_cards.reduce( + (acc, next) => acc + next.balance, + 0 + ) + return Math.min(giftCardable, toReturn) + } + /** * Calculates the total discount amount for each of the different supported * discount types. If discounts aren't present or invalid returns 0. * @param {Cart} Cart - the cart to calculate discounts for * @return {int} the total discounts amount */ - async getDiscountTotal(cart) { + getDiscountTotal(cart) { let subtotal = this.getSubtotal(cart, { excludeNonDiscounts: true }) if (!cart.discounts || !cart.discounts.length) { return 0 } - // filter out invalid discounts - cart.discounts = cart.discounts.filter(d => { - // !ends_at implies that the discount never expires - // therefore, we do the check following check - if (d.ends_at) { - const parsedEnd = new Date(d.ends_at) - const now = new Date() - return ( - parsedEnd.getTime() > now.getTime() && - d.regions.includes(cart.region_id) - ) - } else { - return d.regions && d.regions.includes(cart.region_id) - } - }) - // we only support having free shipping and one other discount, so first // find the discount, which is not free shipping. const discount = cart.discounts.find( - ({ discount_rule }) => discount_rule.type !== "free_shipping" + ({ rule }) => rule.type !== "free_shipping" ) if (!discount) { return 0 } - const { type, allocation, value } = discount.discount_rule + const { type, allocation, value } = discount.rule let toReturn = 0 if (type === "percentage" && allocation === "total") { @@ -309,6 +314,10 @@ class TotalsService extends BaseService { toReturn = _.sumBy(itemFixedDiscounts, d => d.amount) } + if (subtotal < 0) { + return this.rounded(Math.max(subtotal, toReturn)) + } + return this.rounded(Math.min(subtotal, toReturn)) } diff --git a/packages/medusa/src/services/transaction.js b/packages/medusa/src/services/transaction.js new file mode 100644 index 0000000000..5af02ae3b0 --- /dev/null +++ b/packages/medusa/src/services/transaction.js @@ -0,0 +1,15 @@ +import { BaseService } from "medusa-interfaces" +import mongoose from "mongoose" +import _ from "lodash" + +class TransactionService extends BaseService { + constructor({}) { + super() + } + + async createSession() { + return mongoose.startSession() + } +} + +export default TransactionService diff --git a/packages/medusa/src/services/user.js b/packages/medusa/src/services/user.js index f63d54a771..e41a370086 100644 --- a/packages/medusa/src/services/user.js +++ b/packages/medusa/src/services/user.js @@ -13,32 +13,33 @@ class UserService extends BaseService { PASSWORD_RESET: "user.password_reset", } - constructor({ userModel, eventBusService }) { + constructor({ userRepository, eventBusService, manager }) { super() - /** @private @const {UserModel} */ - this.userModel_ = userModel + /** @private @const {UserRepository} */ + this.userRepository_ = userRepository /** @private @const {EventBus} */ this.eventBus_ = eventBusService + + /** @private @const {EntityManager} */ + this.manager_ = manager } - /** - * Used to validate user ids. Throws an error if the cast fails - * @param {string} rawId - the raw user id to validate. - * @return {string} the validated id - */ - validateId_(rawId) { - const schema = Validator.objectId() - const { value, error } = schema.validate(rawId.toString()) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "The userId could not be casted to an ObjectId" - ) + withTransaction(transactionManager) { + if (!transactionManager) { + return this } - return value + const cloned = new UserService({ + manager: transactionManager, + userRepository: this.userRepository_, + eventBusService: this.eventBus_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned } /** @@ -47,26 +48,16 @@ class UserService extends BaseService { * @return {string} the validated email */ validateEmail_(email) { - const schema = Validator.string() - .email() - .required() - const { value, error } = schema.validate(email) - if (error) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The email is not valid" - ) - } - - return value + return email } /** * @param {Object} selector - the query object for find * @return {Promise} the result of the find operation */ - list(selector) { - return this.userModel_.find(selector) + async list(selector) { + const userRepo = this.manager_.getCustomRepository(this.userRepository_) + return userRepo.find({ where: selector }) } /** @@ -75,13 +66,13 @@ class UserService extends BaseService { * @param {string} userId - the id of the user to get. * @return {Promise} the user document. */ - async retrieve(userId) { + async retrieve(userId, config = {}) { + const userRepo = this.manager_.getCustomRepository(this.userRepository_) + const validatedId = this.validateId_(userId) - const user = await this.userModel_ - .findOne({ _id: validatedId }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + const query = this.buildQuery_({ id: validatedId }, config) + + const user = await userRepo.findOne(query) if (!user) { throw new MedusaError( @@ -89,6 +80,7 @@ class UserService extends BaseService { `User with id: ${userId} was not found` ) } + return user } @@ -98,12 +90,13 @@ class UserService extends BaseService { * @param {string} apiToken - the token of the user to get. * @return {Promise} the user document. */ - async retrieveByApiToken(apiToken) { - const user = await this.userModel_ - .findOne({ api_token: apiToken }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + async retrieveByApiToken(apiToken, relations = []) { + const userRepo = this.manager_.getCustomRepository(this.userRepository_) + + const user = await userRepo.findOne({ + where: { api_token: apiToken }, + relations, + }) if (!user) { throw new MedusaError( @@ -111,6 +104,7 @@ class UserService extends BaseService { `User with api token: ${apiToken} was not found` ) } + return user } @@ -120,9 +114,12 @@ class UserService extends BaseService { * @param {string} email - the email of the user to get. * @return {Promise} the user document. */ - async retrieveByEmail(email) { - const user = await this.userModel_.findOne({ email }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + async retrieveByEmail(email, relations = []) { + const userRepo = this.manager_.getCustomRepository(this.userRepository_) + + const user = await userRepo.findOne({ + where: { email }, + relations, }) if (!user) { @@ -131,6 +128,7 @@ class UserService extends BaseService { `User with email: ${email} was not found` ) } + return user } @@ -151,12 +149,17 @@ class UserService extends BaseService { * @return {Promise} the result of create */ async create(user, password) { - const validatedEmail = this.validateEmail_(user.email) - const hashedPassword = await this.hashPassword_(password) - user.email = validatedEmail - user.password_hash = hashedPassword - return this.userModel_.create(user).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + return this.atomicPhase_(async manager => { + const userRepo = manager.getCustomRepository(this.userRepository_) + + const validatedEmail = this.validateEmail_(user.email) + const hashedPassword = await this.hashPassword_(password) + user.email = validatedEmail + user.password_hash = hashedPassword + + const created = await userRepo.create(user) + + return userRepo.save(created) }) } @@ -166,38 +169,38 @@ class UserService extends BaseService { * @return {Promise} the result of create */ async update(userId, update) { - const validatedId = this.validateId_(userId) + return this.atomicPhase_(async manager => { + const userRepo = manager.getCustomRepository(this.userRepository_) + const validatedId = this.validateId_(userId) - if (update.password_hash) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use dedicated methods, `setPassword`, `generateResetPasswordToken` for password operations" - ) - } + const user = await this.retrieve(validatedId) - if (update.email) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "You are not allowed to update email" - ) - } + const { email, password_hash, metadata, ...rest } = update - if (update.metadata) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Use setMetadata to update metadata fields" - ) - } + if (email) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "You are not allowed to update email" + ) + } - return this.userModel_ - .updateOne( - { _id: validatedId }, - { $set: update }, - { runValidators: true } - ) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) + if (password_hash) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Use dedicated methods, `setPassword`, `generateResetPasswordToken` for password operations" + ) + } + + if (metadata) { + user.metadata = this.setMetadata_(user, metadata) + } + + for (const [key, value] of Object.entries(rest)) { + user[key] = value + } + + return userRepo.save(user) + }) } /** @@ -207,16 +210,17 @@ class UserService extends BaseService { * @return {Promise} the result of the delete operation. */ async delete(userId) { - let user - try { - user = await this.retrieve(userId) - } catch (error) { - // delete is idempotent, but we return a promise to allow then-chaining - return Promise.resolve() - } + return this.atomicPhase_(async manager => { + const userRepo = manager.getCustomRepository(this.userRepository_) - return this.userModel_.deleteOne({ _id: user._id }).catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + // Should not fail, if user does not exist, since delete is idempotent + const user = await userRepo.findOne({ where: { id: userId } }) + + if (!user) return Promise.resolve() + + await userRepo.softRemove(user) + + return Promise.resolve() }) } @@ -228,21 +232,24 @@ class UserService extends BaseService { * @param {string} password - the old password to set * @returns {Promise} the result of the update operation */ - async setPassword(userId, password) { - const user = await this.retrieve(userId) + async setPassword_(userId, password) { + return this.atomicPhase_(async manager => { + const userRepo = manager.getCustomRepository(this.userRepository_) - const hashedPassword = await this.hashPassword_(password) - if (!hashedPassword) { - throw new MedusaError( - MedusaError.Types.DB_ERROR, - `An error occured while hashing password` - ) - } + const user = await this.retrieve(userId) - return this.userModel_.updateOne( - { _id: user._id }, - { $set: { password_hash: hashedPassword } } - ) + const hashedPassword = await this.hashPassword_(password) + if (!hashedPassword) { + throw new MedusaError( + MedusaError.Types.DB_ERROR, + `An error occured while hashing password` + ) + } + + user.password_hash = hashedPassword + + return userRepo.save(user) + }) } /** @@ -258,7 +265,7 @@ class UserService extends BaseService { const user = await this.retrieve(userId) const secret = user.password_hash const expiry = Math.floor(Date.now() / 1000) + 60 * 15 - const payload = { user_id: user._id, exp: expiry } + const payload = { user_id: user.id, exp: expiry } const token = jwt.sign(payload, secret) // Notify subscribers this.eventBus_.emit(UserService.Events.PASSWORD_RESET, { @@ -276,62 +283,11 @@ class UserService extends BaseService { * @return {User} return the decorated user. */ async decorate(user, fields, expandFields = []) { - const requiredFields = ["_id", "metadata"] + const requiredFields = ["id", "metadata"] const decorated = _.pick(user, fields.concat(requiredFields)) const final = await this.runDecorators_(decorated) return final } - - /** - * Dedicated method to set metadata for a user. - * To ensure that plugins does not overwrite each - * others metadata fields, setMetadata is provided. - * @param {string} userId - the user to apply metadata to. - * @param {string} key - key for metadata field - * @param {string} value - value for metadata field. - * @return {Promise} resolves to the updated result. - */ - async setMetadata(userId, key, value) { - const validatedId = this.validateId_(userId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.userModel_ - .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } - - /** - * Dedicated method to delete metadata for a user. - * @param {string} userId - the user to delete metadata from. - * @param {string} key - key for metadata field - * @return {Promise} resolves to the updated result. - */ - async deleteMetadata(userId, key) { - const validatedId = this.validateId_(userId) - - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - - const keyPath = `metadata.${key}` - return this.userModel_ - .updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } }) - .catch(err => { - throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) - }) - } } export default UserService diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index cfb07214c5..2970d53eeb 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -1,101 +1,58 @@ class OrderSubscriber { constructor({ - paymentProviderService, - customerService, + manager, eventBusService, discountService, + giftCardService, totalsService, orderService, regionService, }) { + this.manager_ = manager this.totalsService_ = totalsService - this.paymentProviderService_ = paymentProviderService - - this.customerService_ = customerService - this.discountService_ = discountService + this.giftCardService_ = giftCardService + this.orderService_ = orderService this.regionService_ = regionService this.eventBus_ = eventBusService - this.eventBus_.subscribe("order.placed", async order => { - await this.customerService_.addOrder(order.customer_id, order._id) + this.eventBus_.subscribe("order.placed", this.handleOrderPlaced) + } - const address = { - ...order.shipping_address, - } - delete address._id - - await this.customerService_.addAddress(order.customer_id, address) + handleOrderPlaced = async data => { + const order = await this.orderService_.retrieve(data.id, { + select: ["subtotal"], + relations: ["discounts", "items", "gift_cards"], }) - this.eventBus_.subscribe("order.placed", this.handleDiscounts) - - this.eventBus_.subscribe("order.placed", this.handleGiftCards) - } - - handleDiscounts = async order => { await Promise.all( - order.discounts.map(async d => { - const subtotal = await this.totalsService_.getSubtotal(order) - if (d.is_giftcard) { - const discountRule = { - ...d.discount_rule, - value: Math.max(0, d.discount_rule.value - subtotal), - } - - delete discountRule._id - - return this.discountService_.update(d._id, { - discount_rule: discountRule, - usage_count: d.usage_count + 1, - disabled: discountRule.value === 0, - }) - } else { - return this.discountService_.update(d._id, { - usage_count: d.usage_count + 1, - }) - } - }) - ) - } - - handleGiftCards = async order => { - const region = await this.regionService_.retrieve(order.region_id) - const items = await Promise.all( order.items.map(async i => { if (i.is_giftcard) { - const giftcard = await this.discountService_ - .generateGiftCard(i.content.unit_price, region._id) - .then(result => { - this.eventBus_.emit("order.gift_card_created", { - line_item: i, - currency_code: region.currency_code, - tax_rate: region.tax_rate, - giftcard: result, - email: order.email, - }) - return result + for (let qty = 0; qty < i.quantity; qty++) { + await this.giftCardService_.create({ + region_id: order.region_id, + order_id: order.id, + value: i.unit_price, + balance: i.unit_price, + metadata: i.metadata, }) - return { - ...i, - metadata: { - ...i.metadata, - giftcard: giftcard._id, - }, } } - return i }) ) - return this.orderService_.update(order._id, { - items, - }) + await Promise.all( + order.discounts.map(async d => { + return this.discountService_.update(d.id, { + usage_count: d.usage_count + 1, + }) + }) + ) } } diff --git a/packages/medusa/src/utils/countries.js b/packages/medusa/src/utils/countries.js index 5364796a63..20f91a1a04 100644 --- a/packages/medusa/src/utils/countries.js +++ b/packages/medusa/src/utils/countries.js @@ -270,7 +270,6 @@ export const countries = [ { alpha2: "PR", name: "Puerto Rico", alpha3: "PRI", numeric: "630" }, { alpha2: "QA", name: "Qatar", alpha3: "QAT", numeric: "634" }, { alpha2: "RE", name: "Reunion", alpha3: "REU", numeric: "638" }, - { alpha2: "RO", name: "Romania", alpha3: "ROU", numeric: "642" }, { alpha2: "RO", name: "Romania", alpha3: "ROM", numeric: "642" }, { alpha2: "RU", name: "Russian Federation", alpha3: "RUS", numeric: "643" }, { alpha2: "RW", name: "Rwanda", alpha3: "RWA", numeric: "646" }, diff --git a/packages/medusa/src/utils/naming-strategy.ts b/packages/medusa/src/utils/naming-strategy.ts new file mode 100644 index 0000000000..18b151a1e8 --- /dev/null +++ b/packages/medusa/src/utils/naming-strategy.ts @@ -0,0 +1,13 @@ +import { DefaultNamingStrategy } from "typeorm" + +export class ShortenedNamingStrategy extends DefaultNamingStrategy { + eagerJoinRelationAlias(alias: string, propertyPath: string): string { + const path = propertyPath + .split(".") + .map(p => p.substring(0, 2)) + .join("_") + let out = alias + "_" + path + let match = out.match(/_/g) || [] + return out + match.length + } +} diff --git a/packages/medusa/tsconfig.json b/packages/medusa/tsconfig.json new file mode 100644 index 0000000000..958846589c --- /dev/null +++ b/packages/medusa/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": [ + "es5", + "es6" + ], + "target": "es5", + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true + } +} diff --git a/packages/medusa/yarn.lock b/packages/medusa/yarn.lock index e0e0711d10..140672b399 100644 --- a/packages/medusa/yarn.lock +++ b/packages/medusa/yarn.lock @@ -25,7 +25,18 @@ dependencies: "@babel/highlight" "^7.0.0" +<<<<<<< HEAD +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/code-frame@^7.12.11": +======= "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11": +>>>>>>> master version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== @@ -73,6 +84,18 @@ jsesc "^2.5.1" source-map "^0.5.0" +<<<<<<< HEAD +"@babel/generator@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" + integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== + dependencies: + "@babel/types" "^7.12.5" + jsesc "^2.5.1" + source-map "^0.5.0" + +======= +>>>>>>> master "@babel/generator@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" @@ -230,7 +253,20 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +<<<<<<< HEAD +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-function-name@^7.12.11": +======= "@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11": +>>>>>>> master version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== @@ -257,6 +293,16 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.9.5" +<<<<<<< HEAD +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +======= +>>>>>>> master "@babel/helper-get-function-arity@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" @@ -292,7 +338,11 @@ dependencies: "@babel/types" "^7.7.4" +<<<<<<< HEAD +"@babel/helper-member-expression-to-functions@^7.12.1": +======= "@babel/helper-member-expression-to-functions@^7.12.1", "@babel/helper-member-expression-to-functions@^7.12.7": +>>>>>>> master version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855" integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw== @@ -354,12 +404,21 @@ "@babel/types" "^7.7.4" lodash "^4.17.13" +<<<<<<< HEAD +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz#7f94ae5e08721a49467346aa04fd22f750033b9c" + integrity sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw== + dependencies: + "@babel/types" "^7.12.7" +======= "@babel/helper-optimise-call-expression@^7.10.4", "@babel/helper-optimise-call-expression@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d" integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ== dependencies: "@babel/types" "^7.12.10" +>>>>>>> master "@babel/helper-optimise-call-expression@^7.7.4": version "7.7.4" @@ -418,6 +477,16 @@ "@babel/types" "^7.7.4" "@babel/helper-replace-supers@^7.12.1": +<<<<<<< HEAD + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9" + integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" +======= version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d" integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA== @@ -426,6 +495,7 @@ "@babel/helper-optimise-call-expression" "^7.12.10" "@babel/traverse" "^7.12.10" "@babel/types" "^7.12.11" +>>>>>>> master "@babel/helper-replace-supers@^7.7.4": version "7.7.4" @@ -469,7 +539,18 @@ dependencies: "@babel/types" "^7.12.1" +<<<<<<< HEAD +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-split-export-declaration@^7.12.11": +======= "@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11": +>>>>>>> master version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== @@ -490,7 +571,16 @@ dependencies: "@babel/types" "^7.8.3" +<<<<<<< HEAD +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helper-validator-identifier@^7.12.11": +======= "@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": +>>>>>>> master version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== @@ -500,7 +590,16 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== +<<<<<<< HEAD +"@babel/helper-validator-option@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9" + integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A== + +"@babel/helper-validator-option@^7.12.11": +======= "@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": +>>>>>>> master version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw== @@ -579,11 +678,23 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71" integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig== +<<<<<<< HEAD +"@babel/parser@^7.12.11": +======= "@babel/parser@^7.12.11", "@babel/parser@^7.12.7": +>>>>>>> master version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== +<<<<<<< HEAD +"@babel/parser@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" + integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== + +======= +>>>>>>> master "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" @@ -623,6 +734,18 @@ "@babel/helper-create-class-features-plugin" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" +<<<<<<< HEAD +"@babel/plugin-proposal-decorators@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.1.tgz#59271439fed4145456c41067450543aee332d15f" + integrity sha512-knNIuusychgYN8fGJHONL0RbFxLGawhXOJNLBk75TniTsZZeA+wdkDuv6wp4lGwzQEKjZi6/WYtnb3udNPmQmQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators" "^7.12.1" + +======= +>>>>>>> master "@babel/plugin-proposal-dynamic-import@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc" @@ -788,6 +911,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-syntax-decorators@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz#81a8b535b284476c41be6de06853a8802b98c5dd" + integrity sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-dynamic-import@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.7.4.tgz#29ca3b4415abfe4a5ec381e903862ad1a54c3aec" @@ -907,6 +1037,16 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +<<<<<<< HEAD +"@babel/plugin-syntax-typescript@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz#460ba9d77077653803c3dd2e673f76d66b4029e5" + integrity sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +======= +>>>>>>> master "@babel/plugin-transform-arrow-functions@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3" @@ -1424,6 +1564,18 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +<<<<<<< HEAD +"@babel/plugin-transform-typescript@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz#d92cc0af504d510e26a754a7dbc2e5c8cd9c7ab4" + integrity sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.12.1" + +======= +>>>>>>> master "@babel/plugin-transform-unicode-escapes@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709" @@ -1587,6 +1739,18 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +<<<<<<< HEAD +"@babel/preset-typescript@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz#fc7df8199d6aae747896f1e6c61fc872056632a3" + integrity sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.1" + "@babel/plugin-transform-typescript" "^7.12.1" + +======= +>>>>>>> master "@babel/register@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.7.4.tgz#45a4956471a9df3b012b747f5781cc084ee8f128" @@ -1654,7 +1818,11 @@ globals "^11.1.0" lodash "^4.17.13" +<<<<<<< HEAD +"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1": +======= "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10": +>>>>>>> master version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== @@ -1669,6 +1837,24 @@ globals "^11.1.0" lodash "^4.17.19" +<<<<<<< HEAD +"@babel/traverse@^7.12.5": + version "7.12.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f" + integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +======= +>>>>>>> master "@babel/traverse@^7.8.6": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" @@ -1693,7 +1879,20 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +<<<<<<< HEAD +"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.5", "@babel/types@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" + integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.4.4": +======= "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.4": +>>>>>>> master version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== @@ -1738,32 +1937,20 @@ resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== -"@hapi/address@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" - integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/formula@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd" integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA== -"@hapi/formula@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128" - integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A== - "@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0": version "8.5.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.0.tgz#2f9ce301c8898e1c3248b0a8564696b24d1a9a5a" integrity sha512-7XYT10CZfPsH7j9F1Jmg1+d0ezOux2oM2GfArAzLwWe4mE2Dr3hVjsAL6+TFY49RRJlCdJDMw3nJsLFroTc8Kw== "@hapi/hoek@^9.0.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" - integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== + version "9.1.1" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa" + integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw== "@hapi/joi@^16.1.8": version "16.1.8" @@ -1781,11 +1968,6 @@ resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13" integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ== -"@hapi/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df" - integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw== - "@hapi/topo@^3.1.3": version "3.1.6" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" @@ -1983,6 +2165,23 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@sideway/address@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d" + integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" + integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sinonjs/commons@^1.7.0": version "1.7.2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" @@ -1990,6 +2189,11 @@ dependencies: type-detect "4.0.8" +"@sqltools/formatter@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.2.tgz#9390a8127c0dcba61ebd7fdcc748655e191bdd68" + integrity sha512-/5O7Fq6Vnv8L6ucmPjaWbVG1XkP4FO+w5glqfkIsq3Xw4oyNAdJddbnYodNDAfjVUvo/rrSCTom4kAND7T1o5Q== + "@types/babel__core@^7.1.7": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -2023,6 +2227,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/bson@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.3.tgz#30889d2ffde6262abbe38659364c631454999fbf" + integrity sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw== + dependencies: + "@types/node" "*" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -2055,6 +2266,14 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/mongodb@^3.5.27": + version "3.6.3" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.3.tgz#5655af409d9e32d5d5ae9a653abf3e5f9c83eb7a" + integrity sha512-6YNqGP1hk5bjUFaim+QoFFuI61WjHiHE1BNeB41TA00Xd2K7zG4lcWyLLq/XtIp36uMavvS5hoAUJ+1u/GcX2Q== + dependencies: + "@types/bson" "*" + "@types/node" "*" + "@types/node@*": version "13.13.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c" @@ -2177,6 +2396,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2192,6 +2416,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2208,6 +2437,11 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +app-root-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" + integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== + append-field@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" @@ -2397,10 +2631,17 @@ babel-preset-jest@^25.5.0: babel-plugin-jest-hoist "^25.5.0" babel-preset-current-node-syntax "^0.1.2" +<<<<<<< HEAD +babel-preset-medusa-package@^1.0.2-alpha.550+5fe9cd6: + version "1.0.2-alpha.551" + resolved "https://registry.yarnpkg.com/babel-preset-medusa-package/-/babel-preset-medusa-package-1.0.2-alpha.551.tgz#50082b6fb8c0c40ad045d1aeae63923e3742c753" + integrity sha512-Bk5aFFalN4CYPdF/V5MbkHM0UDZo1ggFI5PfRNl77Rrrgx060Lc0cX5igZWCiPrIKuPmFoUazTisEqC+5Q6gqQ== +======= babel-preset-medusa-package@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-medusa-package/-/babel-preset-medusa-package-1.0.1.tgz#37e243149f79c079385a4e6c30b29c3171eb93e2" integrity sha512-y1lE8sHREEBcV04EEwL5cgXR4CCmz9spH4BCn442SkfM3sAA2xRzVvGOvtYAFzg2zOL4DxjaUd8QCmI8eDRfHw== +>>>>>>> master dependencies: "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-optional-chaining" "^7.12.1" @@ -2415,6 +2656,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -2452,6 +2698,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== +bl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" + integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + bluebird@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -2529,6 +2783,18 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" +<<<<<<< HEAD +browserslist@^4.14.5, browserslist@^4.16.0: + version "4.16.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.1.tgz#bf757a2da376b3447b800a16f0f1c96358138766" + integrity sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA== + dependencies: + caniuse-lite "^1.0.30001173" + colorette "^1.2.1" + electron-to-chromium "^1.3.634" + escalade "^3.1.1" + node-releases "^1.1.69" +======= browserslist@^4.14.5, browserslist@^4.15.0: version "4.16.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b" @@ -2539,6 +2805,7 @@ browserslist@^4.14.5, browserslist@^4.15.0: electron-to-chromium "^1.3.621" escalade "^3.1.1" node-releases "^1.1.67" +>>>>>>> master browserslist@^4.6.0, browserslist@^4.8.0: version "4.8.2" @@ -2556,10 +2823,10 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@^1.1.1, bson@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" - integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== +bson@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.5.tgz#2aaae98fcdf6750c0848b0cba1ddec3c73060a34" + integrity sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg== buffer-equal-constant-time@1.0.1: version "1.0.1" @@ -2571,6 +2838,19 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + bull@^3.12.1: version "3.12.1" resolved "https://registry.yarnpkg.com/bull/-/bull-3.12.1.tgz#ced62d0afca81c9264b44f1b6f39243df5d2e73f" @@ -2643,10 +2923,17 @@ caniuse-lite@^1.0.30001015: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001015.tgz#15a7ddf66aba786a71d99626bc8f2b91c6f0f5f0" integrity sha512-/xL2AbW/XWHNu1gnIrO8UitBGoFthcsDgU9VLK1/dpsoxbaD5LscHozKze05R6WLsBvLhqv78dAPozMFQBYLbQ== +<<<<<<< HEAD +caniuse-lite@^1.0.30001173: + version "1.0.30001176" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001176.tgz#e44bac506d4656bae4944a1417f41597bd307335" + integrity sha512-VWdkYmqdkDLRe0lvfJlZQ43rnjKqIGKHWhWWRbkqMsJIUaYDNf/K/sdZZcVO6YKQklubokdkJY+ujArsuJ5cag== +======= caniuse-lite@^1.0.30001165: version "1.0.30001170" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7" integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA== +>>>>>>> master capture-exit@^2.0.0: version "2.0.0" @@ -2665,6 +2952,17 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chalk@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2682,6 +2980,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -2773,6 +3079,18 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-highlight@^2.1.4: + version "2.1.9" + resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.9.tgz#4f4ecb05326d70d56d4b4249fabf9a70fb002497" + integrity sha512-t8RNIZgiI24i/mslZ8XT8o660RUj5ZbUJpEZrZa/BNekTzdC2LfMRAnt0Y7sgzNM4FGW5tmWg/YnbTH8o1eIOQ== + dependencies: + chalk "^4.0.0" + highlight.js "^10.0.0" + mz "^2.4.0" + parse5 "^5.1.1" + parse5-htmlparser2-tree-adapter "^6.0.0" + yargs "^15.0.0" + cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -2787,6 +3105,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cluster-key-slot@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" @@ -3003,11 +3330,19 @@ core-js-compat@^3.4.7: semver "^6.3.0" core-js-compat@^3.8.0: +<<<<<<< HEAD + version "3.8.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.2.tgz#3717f51f6c3d2ebba8cbf27619b57160029d1d4c" + integrity sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ== + dependencies: + browserslist "^4.16.0" +======= version "3.8.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e" integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ== dependencies: browserslist "^4.15.0" +>>>>>>> master semver "7.0.0" core-js@^3.2.1: @@ -3021,9 +3356,15 @@ core-js@^3.6.5: integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== core-js@^3.7.0: +<<<<<<< HEAD + version "3.8.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.2.tgz#0a1fd6709246da9ca8eff5bb0cbd15fba9ac7044" + integrity sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A== +======= version "3.8.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.1.tgz#f51523668ac8a294d1285c3b9db44025fda66d47" integrity sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg== +>>>>>>> master core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -3332,10 +3673,17 @@ electron-to-chromium@^1.3.322: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz#a6f7e1c79025c2b05838e8e344f6e89eb83213a8" integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA== +<<<<<<< HEAD +electron-to-chromium@^1.3.634: + version "1.3.637" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.637.tgz#be46f77acc217cdecf633bbd25292f6a36cc689b" + integrity sha512-924WXYMYquYybc+7pNApGlhY2RWg3MY3he4BrZ5BUmM2n1MGBsrS+PZxrlo6UAsWuNl4NE66fqFdwsWkBUGgkA== +======= electron-to-chromium@^1.3.621: version "1.3.633" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe" integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA== +>>>>>>> master emoji-regex@^7.0.1: version "7.0.3" @@ -3430,7 +3778,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -3765,6 +4113,11 @@ fecha@^2.3.3: resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== +figlet@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.0.tgz#2db4d00a584e5155a96080632db919213c3e003c" + integrity sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww== + figures@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" @@ -3961,7 +4314,7 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -4095,6 +4448,13 @@ har-validator@~5.1.3: ajv "^6.5.5" har-schema "^2.0.0" +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4153,6 +4513,11 @@ has@^1.0.1, has@^1.0.3: dependencies: function-bind "^1.1.1" +highlight.js@^10.0.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.0.tgz#ef3ce475e5dfa7a48484260b49ea242ddab823a0" + integrity sha512-EfrUGcQ63oLJbj0J0RI9ebX6TAITbsDBLbsjr881L/X5fMO9+oadKzEF21C7R3ULKG6Gv3uoab2HiqVJa/4+oA== + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -4220,6 +4585,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -4271,7 +4641,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4614,6 +4984,11 @@ is-wsl@^2.1.1: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d" integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog== +is_js@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" + integrity sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0= + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5059,16 +5434,16 @@ joi-objectid@^3.0.1: resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== -joi@^17.2.1: - version "17.2.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a" - integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA== +joi@^17.2.1, joi@^17.3.0: + version "17.3.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2" + integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg== dependencies: - "@hapi/address" "^4.1.0" - "@hapi/formula" "^2.0.0" "@hapi/hoek" "^9.0.0" - "@hapi/pinpoint" "^2.0.0" "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.0" + "@sideway/formula" "^3.0.0" + "@sideway/pinpoint" "^2.0.0" js-levenshtein@^1.1.3: version "1.1.6" @@ -5088,6 +5463,14 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -5210,10 +5593,10 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -kareem@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" - integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== +kareem@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" + integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ== kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" @@ -5448,7 +5831,19 @@ medusa-core-utils@^1.0.11: joi "^17.2.1" joi-objectid "^3.0.1" +<<<<<<< HEAD +medusa-core-utils@^1.0.12-alpha.550+5fe9cd6: + version "1.0.12-alpha.551" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.12-alpha.551.tgz#b6c4d0813e3c4cb16ff6f92b2f75278ed3a2e8c2" + integrity sha512-zhuOEyZEiKHr0k7R3qsArRIJoZwQQSoEb8sgKNYHhNVTj/bB5gRoCs7u7/Erd35sbFMwo/Rb5SjOwely66D8jQ== + dependencies: + joi "^17.2.1" + joi-objectid "^3.0.1" + +medusa-test-utils@^1.0.12-alpha.32+5fe9cd6: +======= medusa-test-utils@^1.0.13: +>>>>>>> master version "1.0.13" resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.13.tgz#f728b8dacb1ba64d85529e3f2e9dba6c7e315416" integrity sha512-EBbjGLHFEG/Fnqneire/bNCPeseo1ZFF3n3yMUfp0JDmBAVgE8L52wJa8Zeu36TL/rbQsQAsTAS6La46A8IJgg== @@ -5573,6 +5968,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + moment-timezone@^0.5.25: version "0.5.27" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.27.tgz#73adec8139b6fe30452e78f210f27b1f346b8877" @@ -5585,12 +5985,14 @@ moment-timezone@^0.5.25: resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== -mongodb@3.3.5: - version "3.3.5" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.5.tgz#38d531013afede92b0dd282e3b9f3c08c9bdff3b" - integrity sha512-6NAv5gTFdwRyVfCz+O+KDszvjpyxmZw+VlmqmqKR2GmpkeKrKFRv/ZslgTtZba2dc9JYixIf99T5Gih7TIWv7Q== +mongodb@3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.3.tgz#eddaed0cc3598474d7a15f0f2a5b04848489fd05" + integrity sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w== dependencies: - bson "^1.1.1" + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" require_optional "^1.0.1" safe-buffer "^5.1.2" optionalDependencies: @@ -5602,19 +6004,20 @@ mongoose-legacy-pluralize@1.0.2: integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== mongoose@^5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.8.0.tgz#ddbfd6849632ed5840ec3e5faeaab1945bb3b650" - integrity sha512-+VqrLGmHoDW/72yaXgiXSF7E/JcZ8Iyt7etrd4x3+Bj0z7k6GHHUBgGHP5ySPoG4J412RFuvHqx6xEOIuUrcfQ== + version "5.11.11" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.11.11.tgz#f0f1b3691385aa8cf0e4acdec20f1851bb6f9660" + integrity sha512-JgKKAosJf6medPOZi2LmO7sMz7Sg00mgjyPAKari3alzL+R/n8D+zKK29iGtJpNNtv9IKy14H37CWuiaZ7016w== dependencies: - bson "~1.1.1" - kareem "2.3.1" - mongodb "3.3.5" + "@types/mongodb" "^3.5.27" + bson "^1.1.4" + kareem "2.3.2" + mongodb "3.6.3" mongoose-legacy-pluralize "1.0.2" - mpath "0.6.0" - mquery "3.2.2" + mpath "0.8.3" + mquery "3.2.3" ms "2.1.2" regexp-clone "1.0.0" - safe-buffer "5.1.2" + safe-buffer "5.2.1" sift "7.0.1" sliced "1.0.1" @@ -5629,15 +6032,15 @@ morgan@^1.9.1: on-finished "~2.3.0" on-headers "~1.0.1" -mpath@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" - integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== +mpath@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.3.tgz#828ac0d187f7f42674839d74921970979abbdd8f" + integrity sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA== -mquery@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" - integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== +mquery@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.3.tgz#bcf54fdfe3baf57b6a22f9b62b1ad5fa18ffe96a" + integrity sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g== dependencies: bluebird "3.5.1" debug "3.1.0" @@ -5679,6 +6082,15 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mz@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -5784,10 +6196,17 @@ node-releases@^1.1.42: dependencies: semver "^6.3.0" +<<<<<<< HEAD +node-releases@^1.1.69: + version "1.1.69" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.69.tgz#3149dbde53b781610cd8b486d62d86e26c3725f6" + integrity sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA== +======= node-releases@^1.1.67: version "1.1.67" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== +>>>>>>> master nodemon@^2.0.1: version "2.0.1" @@ -5894,7 +6313,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -6076,6 +6495,11 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6083,6 +6507,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parent-require@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parent-require/-/parent-require-1.0.0.tgz#746a167638083a860b0eef6732cb27ed46c32977" + integrity sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc= + parse-json@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" @@ -6098,11 +6527,28 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= +parse5-htmlparser2-tree-adapter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + parse5@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parse5@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -6203,6 +6649,57 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +pg-connection-string@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" + integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.2.tgz#a560e433443ed4ad946b84d774b3f22452694dff" + integrity sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA== + +pg-protocol@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.4.0.tgz#43a71a92f6fe3ac559952555aa3335c8cb4908be" + integrity sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.5.1.tgz#34dcb15f6db4a29c702bf5031ef2e1e25a06a120" + integrity sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.4.0" + pg-pool "^3.2.2" + pg-protocol "^1.4.0" + pg-types "^2.1.0" + pgpass "1.x" + +pgpass@1.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.4.tgz#85eb93a83800b20f8057a2b029bf05abaf94ea9c" + integrity sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w== + dependencies: + split2 "^3.1.1" + picomatch@^2.0.4: version "2.1.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.1.1.tgz#ecdfbea7704adb5fe6fb47f9866c4c0e15e905c5" @@ -6254,6 +6751,28 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -6461,6 +6980,15 @@ readable-stream@^2.2.2, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^3.1.1: version "3.4.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" @@ -6530,6 +7058,11 @@ redis@^3.0.2: redis-errors "^1.2.0" redis-parser "^3.0.0" +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -6648,9 +7181,15 @@ regjsparser@^0.6.0: jsesc "~0.5.0" regjsparser@^0.6.4: +<<<<<<< HEAD + version "0.6.6" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.6.tgz#6d8c939d1a654f78859b08ddcc4aa777f3fa800a" + integrity sha512-jjyuCp+IEMIm3N1H1LLTJW1EISEJV9+5oHdEyrt43Pg9cDSb6rrLZei2cVWpl0xTjmmlpec/lEQGYgM7xfpGCQ== +======= version "0.6.4" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== +>>>>>>> master dependencies: jsesc "~0.5.0" @@ -6669,6 +7208,13 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +request-ip@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-2.1.3.tgz#99ab2bafdeaf2002626e28083cb10597511d9e14" + integrity sha512-J3qdE/IhVM3BXkwMIVO4yFrvhJlU3H7JH16+6yHucadT4fePnR8dyh+vEs6FIx0S2x5TCt2ptiPfHcn0sqhbYQ== + dependencies: + is_js "^0.9.0" + request-promise-core@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" @@ -6838,6 +7384,11 @@ safe-buffer@5.2.0, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@5.2.1, safe-buffer@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -6872,7 +7423,7 @@ saslprep@^1.0.0: dependencies: sparse-bitfield "^3.0.3" -sax@^1.2.4: +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -6960,6 +7511,14 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +sha.js@^2.4.11: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -7144,6 +7703,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^3.1.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -7357,6 +7923,11 @@ supertest@^4.0.2: methods "^1.1.2" superagent "^3.8.3" +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7441,6 +8012,20 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" @@ -7546,6 +8131,11 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +tslib@^1.13.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" @@ -7610,6 +8200,28 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typeorm@^0.2.29: + version "0.2.29" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.29.tgz#401289dc91900d72eccb26e31cdb7f0591a2272e" + integrity sha512-ih1vrTe3gEAGKRcWlcsTRxTL7gNjacQE498wVGuJ3ZRujtMqPZlbAWuC7xDzWCRjQnkZYNwZQeG9UgKfxSHB5g== + dependencies: + "@sqltools/formatter" "1.2.2" + app-root-path "^3.0.0" + buffer "^5.5.0" + chalk "^4.1.0" + cli-highlight "^2.1.4" + debug "^4.1.1" + dotenv "^8.2.0" + glob "^7.1.6" + js-yaml "^3.14.0" + mkdirp "^1.0.4" + reflect-metadata "^0.1.13" + sha.js "^2.4.11" + tslib "^1.13.0" + xml2js "^0.4.23" + yargonaut "^1.1.2" + yargs "^16.0.3" + uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" @@ -7617,6 +8229,11 @@ uid-safe@~2.1.5: dependencies: random-bytes "~1.0.0" +ulid@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f" + integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw== + undefsafe@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" @@ -7760,6 +8377,11 @@ uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== +uuid@^8.3.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== + v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" @@ -7922,6 +8544,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7968,6 +8599,19 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -7983,6 +8627,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -7993,7 +8642,16 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yargs-parser@^18.1.1: +yargonaut@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/yargonaut/-/yargonaut-1.1.4.tgz#c64f56432c7465271221f53f5cc517890c3d6e0c" + integrity sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA== + dependencies: + chalk "^1.1.1" + figlet "^1.1.1" + parent-require "^1.0.0" + +yargs-parser@^18.1.1, yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -8001,6 +8659,28 @@ yargs-parser@^18.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs@^15.0.0: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^15.3.1: version "15.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" @@ -8017,3 +8697,16 @@ yargs@^15.3.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.1" + +yargs@^16.0.3: + version "16.1.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.1.1.tgz#5a4a095bd1ca806b0a50d0c03611d38034d219a1" + integrity sha512-hAD1RcFP/wfgfxgMVswPE+z3tlPFtxG8/yWUrG2i17sTWGCGqWnxKcLTF4cUKDUK8fzokwsmO9H0TDkRbMHy8w== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2"