diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..2a889697f0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,9 @@ +{ + "plugins": ["prettier"], + "extends": ["prettier"], + "rules": { + "prettier/prettier": "error", + "semi": "error", + "no-unused-expressions": "true" + } +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..70175ce150 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/docs/api/admin-spec3.json b/docs/api/admin-spec3.json index 9ce086101b..caa5b03d0a 100644 --- a/docs/api/admin-spec3.json +++ b/docs/api/admin-spec3.json @@ -534,6 +534,526 @@ } } }, + "/draft-orders": { + "post": { + "operationId": "PostDraftOrders", + "summary": "Create a Draft Order", + "description": "Creates a Draft Order", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "The status of the draft order", + "type": "string" + }, + "email": { + "description": "The email of the customer of the draft order", + "type": "string" + }, + "billing_address": { + "description": "The Address to be used for billing purposes.", + "anyOf": [ + { + "$ref": "#/components/schemas/address" + } + ] + }, + "shipping_address": { + "description": "The Address to be used for shipping.", + "anyOf": [ + { + "$ref": "#/components/schemas/address" + } + ] + }, + "items": { + "description": "The Line Items that have been received.", + "type": "array", + "items": { + "properties": { + "variant_id": { + "description": "The id of the Product Variant to generate the Line Item from.", + "type": "string" + }, + "unit_price": { + "description": "The potential custom price of the item.", + "type": "integer" + }, + "title": { + "description": "The potential custom title of the item.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the Line Item.", + "type": "integer" + }, + "metadata": { + "description": "The optional key-value map with additional details about the Line Item.", + "type": "object" + } + } + } + }, + "region_id": { + "description": "The id of the region for the draft order", + "type": "string" + }, + "discounts": { + "description": "The discounts to add on the draft order", + "type": "array", + "items": { + "properties": { + "code": { + "description": "The code of the discount to apply", + "type": "string" + } + } + } + }, + "customer_id": { + "description": "The id of the customer to add on the draft order", + "type": "string" + }, + "no_notification_order": { + "description": "An optional flag passed to the resulting order to determine use of notifications.", + "type": "boolean" + }, + "shipping_methods": { + "description": "The shipping methods for the draft order", + "type": "array", + "items": { + "properties": { + "option_id": { + "description": "The id of the shipping option in use", + "type": "string" + }, + "data": { + "description": "The optional additional data needed for the shipping method", + "type": "object" + }, + "price": { + "description": "The potential custom price of the shipping", + "type": "integer" + } + } + } + }, + "metadata": { + "description": "The optional key-value map with additional details about the Draft Order.", + "type": "object" + } + } + } + } + } + }, + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + }, + "get": { + "operationId": "GetDraftOrders", + "summary": "List Draft Orders", + "description": "Retrieves an list of Draft Orders", + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + } + }, + "/draft-orders/{id}/line-items": { + "post": { + "operationId": "PostDraftOrdersDraftOrderLineItems", + "summary": "Create a Line Item for Draft Order", + "description": "Creates a Line Item for the Draft Order", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "variant_id": { + "description": "The id of the Product Variant to generate the Line Item from.", + "type": "string" + }, + "unit_price": { + "description": "The potential custom price of the item.", + "type": "integer" + }, + "title": { + "description": "The potential custom title of the item.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the Line Item.", + "type": "integer" + }, + "metadata": { + "description": "The optional key-value map with additional details about the Line Item.", + "type": "object" + } + } + } + } + } + }, + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + } + }, + "/draft-orders/{id}": { + "delete": { + "operationId": "DeleteDraftOrdersDraftOrder", + "summary": "Delete a Draft Order", + "description": "Deletes a Draft Order", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "The id of the Draft Order.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string", + "description": "The id of the deleted Draft Order." + }, + "object": { + "type": "string", + "description": "The type of the object that was deleted." + }, + "deleted": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "get": { + "operationId": "GetDraftOrdersDraftOrder", + "summary": "Retrieve a Draft Order", + "description": "Retrieves a Draft Order.", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "The id of the Draft Order.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + } + }, + "/draft-orders/{id}/line-items/{line_id}": { + "delete": { + "operationId": "DeleteDraftOrdersDraftOrderLineItemsItem", + "summary": "Delete a Line Item", + "description": "Removes a Line Item from a Draft Order.", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "The id of the Draft Order.", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "line_id", + "required": true, + "description": "The id of the Draft Order.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + }, + "post": { + "operationId": "PostDraftOrdersDraftOrderLineItemsItem", + "summary": "Update a Line Item for a Draft Order", + "description": "Updates a Line Item for a Draft Order", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "unit_price": { + "description": "The potential custom price of the item.", + "type": "integer" + }, + "title": { + "description": "The potential custom title of the item.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the Line Item.", + "type": "integer" + }, + "metadata": { + "description": "The optional key-value map with additional details about the Line Item.", + "type": "object" + } + } + } + } + } + }, + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + } + }, + "/draft-orders/{id}/register-payment": { + "post": { + "summary": "Registers a payment for a Draft Order", + "operationId": "PostDraftOrdersDraftOrderRegisterPayment", + "description": "Registers a payment for a Draft Order.", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "The Draft Order id.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + } + }, + "/admin/draft-orders/{id}": { + "post": { + "operationId": "PostDraftOrdersDraftOrder", + "summary": "Update a Draft Order\"", + "description": "Updates a Draft Order.", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "The id of the Draft Order.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "region_id": { + "type": "string", + "description": "The id of the Region to create the Draft Order in." + }, + "email": { + "type": "string", + "description": "An email to be used on the Draft Order." + }, + "billing_address": { + "description": "The Address to be used for billing purposes.", + "anyOf": [ + { + "$ref": "#/components/schemas/address" + } + ] + }, + "shipping_address": { + "description": "The Address to be used for shipping.", + "anyOf": [ + { + "$ref": "#/components/schemas/address" + } + ] + }, + "discounts": { + "description": "An array of Discount codes to add to the Draft Order.", + "type": "array", + "items": { + "properties": { + "code": { + "description": "The code that a Discount is identifed by.", + "type": "string" + } + } + } + }, + "no_notification_order": { + "description": "An optional flag passed to the resulting order to determine use of notifications.", + "type": "boolean" + }, + "customer_id": { + "description": "The id of the Customer to associate the Draft Order with.", + "type": "string" + } + } + } + } + } + }, + "tags": [ + "Draft Order" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "draft_order": { + "$ref": "#/components/schemas/draft-order" + } + } + } + } + } + } + } + } + }, "/discounts/{id}/regions/{region_id}": { "post": { "operationId": "PostDiscountsDiscountRegionsRegion", @@ -1079,526 +1599,6 @@ } } }, - "/draft-orders": { - "post": { - "operationId": "PostDraftOrders", - "summary": "Create a Draft Order", - "description": "Creates a Draft Order", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "status": { - "description": "The status of the draft order", - "type": "string" - }, - "email": { - "description": "The email of the customer of the draft order", - "type": "string" - }, - "billing_address": { - "description": "The Address to be used for billing purposes.", - "anyOf": [ - { - "$ref": "#/components/schemas/address" - } - ] - }, - "shipping_address": { - "description": "The Address to be used for shipping.", - "anyOf": [ - { - "$ref": "#/components/schemas/address" - } - ] - }, - "items": { - "description": "The Line Items that have been received.", - "type": "array", - "items": { - "properties": { - "variant_id": { - "description": "The id of the Product Variant to generate the Line Item from.", - "type": "string" - }, - "unit_price": { - "description": "The potential custom price of the item.", - "type": "integer" - }, - "title": { - "description": "The potential custom title of the item.", - "type": "string" - }, - "quantity": { - "description": "The quantity of the Line Item.", - "type": "integer" - }, - "metadata": { - "description": "The optional key-value map with additional details about the Line Item.", - "type": "object" - } - } - } - }, - "region_id": { - "description": "The id of the region for the draft order", - "type": "string" - }, - "discounts": { - "description": "The discounts to add on the draft order", - "type": "array", - "items": { - "properties": { - "code": { - "description": "The code of the discount to apply", - "type": "string" - } - } - } - }, - "customer_id": { - "description": "The id of the customer to add on the draft order", - "type": "string" - }, - "no_notification_order": { - "description": "An optional flag passed to the resulting order to determine use of notifications.", - "type": "boolean" - }, - "shipping_methods": { - "description": "The shipping methods for the draft order", - "type": "array", - "items": { - "properties": { - "option_id": { - "description": "The id of the shipping option in use", - "type": "string" - }, - "data": { - "description": "The optional additional data needed for the shipping method", - "type": "object" - }, - "price": { - "description": "The potential custom price of the shipping", - "type": "integer" - } - } - } - }, - "metadata": { - "description": "The optional key-value map with additional details about the Draft Order.", - "type": "object" - } - } - } - } - } - }, - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - }, - "get": { - "operationId": "GetDraftOrders", - "summary": "List Draft Orders", - "description": "Retrieves an list of Draft Orders", - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - } - }, - "/draft-orders/{id}/line-items": { - "post": { - "operationId": "PostDraftOrdersDraftOrderLineItems", - "summary": "Create a Line Item for Draft Order", - "description": "Creates a Line Item for the Draft Order", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "variant_id": { - "description": "The id of the Product Variant to generate the Line Item from.", - "type": "string" - }, - "unit_price": { - "description": "The potential custom price of the item.", - "type": "integer" - }, - "title": { - "description": "The potential custom title of the item.", - "type": "string" - }, - "quantity": { - "description": "The quantity of the Line Item.", - "type": "integer" - }, - "metadata": { - "description": "The optional key-value map with additional details about the Line Item.", - "type": "object" - } - } - } - } - } - }, - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - } - }, - "/draft-orders/{id}": { - "delete": { - "operationId": "DeleteDraftOrdersDraftOrder", - "summary": "Delete a Draft Order", - "description": "Deletes a Draft Order", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "The id of the Draft Order.", - "schema": { - "type": "string" - } - } - ], - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "string", - "description": "The id of the deleted Draft Order." - }, - "object": { - "type": "string", - "description": "The type of the object that was deleted." - }, - "deleted": { - "type": "boolean" - } - } - } - } - } - } - } - }, - "get": { - "operationId": "GetDraftOrdersDraftOrder", - "summary": "Retrieve a Draft Order", - "description": "Retrieves a Draft Order.", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "The id of the Draft Order.", - "schema": { - "type": "string" - } - } - ], - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - } - }, - "/draft-orders/{id}/line-items/{line_id}": { - "delete": { - "operationId": "DeleteDraftOrdersDraftOrderLineItemsItem", - "summary": "Delete a Line Item", - "description": "Removes a Line Item from a Draft Order.", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "The id of the Draft Order.", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "line_id", - "required": true, - "description": "The id of the Draft Order.", - "schema": { - "type": "string" - } - } - ], - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - }, - "post": { - "operationId": "PostDraftOrdersDraftOrderLineItemsItem", - "summary": "Update a Line Item for a Draft Order", - "description": "Updates a Line Item for a Draft Order", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "unit_price": { - "description": "The potential custom price of the item.", - "type": "integer" - }, - "title": { - "description": "The potential custom title of the item.", - "type": "string" - }, - "quantity": { - "description": "The quantity of the Line Item.", - "type": "integer" - }, - "metadata": { - "description": "The optional key-value map with additional details about the Line Item.", - "type": "object" - } - } - } - } - } - }, - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - } - }, - "/draft-orders/{id}/register-payment": { - "post": { - "summary": "Registers a payment for a Draft Order", - "operationId": "PostDraftOrdersDraftOrderRegisterPayment", - "description": "Registers a payment for a Draft Order.", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "The Draft Order id.", - "schema": { - "type": "string" - } - } - ], - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - } - }, - "/admin/draft-orders/{id}": { - "post": { - "operationId": "PostDraftOrdersDraftOrder", - "summary": "Update a Draft Order\"", - "description": "Updates a Draft Order.", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "The id of the Draft Order.", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "region_id": { - "type": "string", - "description": "The id of the Region to create the Draft Order in." - }, - "email": { - "type": "string", - "description": "An email to be used on the Draft Order." - }, - "billing_address": { - "description": "The Address to be used for billing purposes.", - "anyOf": [ - { - "$ref": "#/components/schemas/address" - } - ] - }, - "shipping_address": { - "description": "The Address to be used for shipping.", - "anyOf": [ - { - "$ref": "#/components/schemas/address" - } - ] - }, - "discounts": { - "description": "An array of Discount codes to add to the Draft Order.", - "type": "array", - "items": { - "properties": { - "code": { - "description": "The code that a Discount is identifed by.", - "type": "string" - } - } - } - }, - "no_notification_order": { - "description": "An optional flag passed to the resulting order to determine use of notifications.", - "type": "boolean" - }, - "customer_id": { - "description": "The id of the Customer to associate the Draft Order with.", - "type": "string" - } - } - } - } - } - }, - "tags": [ - "Draft Order" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "draft_order": { - "$ref": "#/components/schemas/draft-order" - } - } - } - } - } - } - } - } - }, "/gift-cards": { "post": { "operationId": "PostGiftCards", @@ -5308,7 +5308,7 @@ } } }, - "/returns/{id}/receive": { + "/returns/{id}receive": { "post": { "operationId": "PostReturnsReturnReceive", "summary": "Receive a Return", @@ -5881,7 +5881,7 @@ }, "delete": { "operationId": "DeleteStoreCurrenciesCode", - "summary": "Remove a Currency Code", + "summary": "Remvoe a Currency Code", "description": "Removes a Currency Code from the available currencies.", "parameters": [ { @@ -6085,7 +6085,7 @@ "/variants": { "get": { "operationId": "GetVariants", - "summary": "List Product Variants", + "summary": "List Product Variants.", "description": "Retrieves a list of Product Variants", "tags": [ "Product Variant" @@ -7546,7 +7546,7 @@ "swaps": { "type": "array", "items": { - "$ref": "#/components/schemas/swap" + "$ref": "#/components/schemas/refund" } }, "gift_card_transactions": { diff --git a/docs/api/admin-spec3.yaml b/docs/api/admin-spec3.yaml index 57e69b6cae..f08a959765 100644 --- a/docs/api/admin-spec3.yaml +++ b/docs/api/admin-spec3.yaml @@ -1070,49 +1070,6 @@ paths: properties: draft_order: $ref: '#/components/schemas/draft-order' - /notifications: - get: - operationId: GetNotifications - summary: List Notifications - description: Retrieves a list of Notifications. - tags: - - Notification - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - notifications: - type: array - items: - $ref: '#/components/schemas/notification' - '/notifications/{id}/resend': - post: - operationId: PostNotificationsNotificationResend - summary: Resend Notification - description: >- - Resends a previously sent notifications, with the same data but - optionally to a different address - parameters: - - in: path - name: id - required: true - description: The id of the Notification - schema: - type: string - tags: - - Notification - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - notification: - $ref: '#/components/schemas/notification' /gift-cards: post: operationId: PostGiftCards @@ -1285,6 +1242,49 @@ paths: properties: gift_card: $ref: '#/components/schemas/gift_card' + /notifications: + get: + operationId: GetNotifications + summary: List Notifications + description: Retrieves a list of Notifications. + tags: + - Notification + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + notifications: + type: array + items: + $ref: '#/components/schemas/notification' + '/notifications/{id}/resend': + post: + operationId: PostNotificationsNotificationResend + summary: Resend Notification + description: >- + Resends a previously sent notifications, with the same data but + optionally to a different address + parameters: + - in: path + name: id + required: true + description: The id of the Notification + schema: + type: string + tags: + - Notification + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + notification: + $ref: '#/components/schemas/notification' '/orders/{id}/shipping-methods': post: operationId: PostOrdersOrderShippingMethods diff --git a/docs/api/store-spec3.json b/docs/api/store-spec3.json index b5b3d50c25..0cac3289cd 100644 --- a/docs/api/store-spec3.json +++ b/docs/api/store-spec3.json @@ -2008,6 +2008,88 @@ } } }, + "/swaps": { + "post": { + "operationId": "PostSwaps", + "summary": "Create a Swap", + "description": "Creates a Swap on an Order by providing some items to return along with some items to send back", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "order_id": { + "type": "string", + "description": "The id of the Order to create the Swap for." + }, + "return_items": { + "description": "The items to include in the Return.", + "type": "array", + "items": { + "properties": { + "item_id": { + "description": "The id of the Line Item from the Order.", + "type": "string" + }, + "quantity": { + "description": "The quantity to return.", + "type": "integer" + } + } + } + }, + "return_shipping": { + "description": "If the Return is to be handled by the store operator the Customer can choose a Return Shipping Method. Alternatvely the Customer can handle the Return themselves.", + "type": "object", + "properties": { + "option_id": { + "type": "string", + "description": "The id of the Shipping Option to create the Shipping Method from." + } + } + }, + "additional_items": { + "description": "The items to exchange the returned items to.", + "type": "array", + "items": { + "properties": { + "variant_id": { + "description": "The id of the Product Variant to send.", + "type": "string" + }, + "quantity": { + "description": "The quantity to send of the variant.", + "type": "integer" + } + } + } + } + } + } + } + } + }, + "tags": [ + "Swap" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "swap": { + "$ref": "#/components/schemas/swap" + } + } + } + } + } + } + } + } + }, "/swaps/{cart_id}": { "get": { "operationId": "GetSwapsSwapCartId", diff --git a/docs/api/store-spec3.yaml b/docs/api/store-spec3.yaml index f9adc8ff10..bc112e95c4 100644 --- a/docs/api/store-spec3.yaml +++ b/docs/api/store-spec3.yaml @@ -40,6 +40,345 @@ tags: servers: - url: 'https://api.medusa-commerce.com/store' paths: + '/customers/{id}/addresses': + post: + operationId: PostCustomersCustomerAddresses + summary: Add a Shipping Address + description: Adds a Shipping Address to a Customer's saved addresses. + parameters: + - in: path + name: id + required: true + description: The Customer id. + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + address: + description: The Address to add to the Customer. + anyOf: + - $ref: '#/components/schemas/address' + tags: + - Customer + responses: + '200': + description: A successful response + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + /customers: + post: + operationId: PostCustomers + summary: Create a Customer + description: Creates a Customer account. + parameters: [] + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + requestBody: + content: + application/json: + schema: + type: object + required: + - email + - first_name + - last_name + - password + properties: + email: + type: string + description: The Customer's email address. + first_name: + type: string + description: The Customer's first name. + last_name: + type: string + description: The Customer's last name. + password: + type: string + description: The Customer's password for login. + phone: + type: string + description: The Customer's phone number. + '/customers/{id}/addresses/{address_id}': + delete: + operationId: DeleteCustomersCustomerAddressesAddress + summary: Delete an Address + description: Removes an Address from the Customer's saved addresse. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + - in: path + name: address_id + required: true + description: The id of the Address to remove. + schema: + type: string + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + post: + operationId: PostCustomersCustomerAddressesAddress + summary: Update a Shipping Address + description: Updates a Customer's saved Shipping Address. + parameters: + - in: path + name: id + required: true + description: The Customer id. + schema: + type: string + - in: path + name: address_id + required: true + description: The id of the Address to update. + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + address: + description: The updated Address. + anyOf: + - $ref: '#/components/schemas/address' + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + '/customers/{id}': + get: + operationId: GetCustomersCustomer + summary: Retrieves a Customer + description: >- + Retrieves a Customer - the Customer must be logged in to retrieve their + details. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + post: + operationId: PostCustomersCustomer + summary: Update Customer details + description: Updates a Customer's saved details. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + first_name: + description: The Customer's first name. + type: string + last_name: + description: The Customer's last name. + type: string + billing_address: + description: The Address to be used for billing purposes. + anyOf: + - $ref: '#/components/schemas/address' + password: + description: The Customer's password. + type: string + phone: + description: The Customer's phone number. + type: string + metadata: + description: Metadata about the customer. + type: object + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + '/customers/{id}/payment-methods': + get: + operationId: GetCustomersCustomerPaymentMethods + summary: Retrieve saved payment methods + description: >- + Retrieves a list of a Customer's saved payment methods. Payment methods + are saved with Payment Providers and it is their responsibility to fetch + saved methods. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + payment_methods: + type: array + items: + properties: + provider_id: + type: string + description: >- + The id of the Payment Provider where the payment + method is saved. + data: + type: object + description: >- + The data needed for the Payment Provider to use the + saved payment method. + '/customers/{id}/orders': + get: + operationId: GetCustomersCustomerOrders + summary: Retrieve Customer Orders + description: Retrieves a list of a Customer's Orders. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + payment_methods: + type: array + items: + $ref: '#/components/schemas/order' + '/customers/{id}/password-token': + post: + operationId: PostCustomersCustomerPasswordToken + summary: Creates a reset password token + description: >- + Creates a reset password token to be used in a subsequent + /reset-password request. The password token should be sent out of band + e.g. via email and will not be returned. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + tags: + - Customer + responses: + '204': + description: OK + '/customers/{id}/reset-password': + post: + operationId: PostCustomersCustomerResetPassword + summary: Resets Customer password + description: >- + Resets a Customer's password using a password token created by a + previous /password-token request. + parameters: + - in: path + name: id + required: true + description: The id of the Customer. + schema: + type: string + tags: + - Customer + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + customer: + $ref: '#/components/schemas/customer' + requestBody: + content: + application/json: + schema: + type: object + required: + - email + - token + - password + properties: + email: + type: string + description: The Customer's email. + token: + type: string + description: The password token created by a /password-token request. + password: + type: string + description: The new password to set for the Customer. '/carts/{id}/shipping-methods': post: operationId: PostCartsCartShippingMethod @@ -546,345 +885,6 @@ paths: properties: cart: $ref: '#/components/schemas/cart' - '/customers/{id}/addresses': - post: - operationId: PostCustomersCustomerAddresses - summary: Add a Shipping Address - description: Adds a Shipping Address to a Customer's saved addresses. - parameters: - - in: path - name: id - required: true - description: The Customer id. - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - address: - description: The Address to add to the Customer. - anyOf: - - $ref: '#/components/schemas/address' - tags: - - Customer - responses: - '200': - description: A successful response - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - /customers: - post: - operationId: PostCustomers - summary: Create a Customer - description: Creates a Customer account. - parameters: [] - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - requestBody: - content: - application/json: - schema: - type: object - required: - - email - - first_name - - last_name - - password - properties: - email: - type: string - description: The Customer's email address. - first_name: - type: string - description: The Customer's first name. - last_name: - type: string - description: The Customer's last name. - password: - type: string - description: The Customer's password for login. - phone: - type: string - description: The Customer's phone number. - '/customers/{id}/addresses/{address_id}': - delete: - operationId: DeleteCustomersCustomerAddressesAddress - summary: Delete an Address - description: Removes an Address from the Customer's saved addresse. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - - in: path - name: address_id - required: true - description: The id of the Address to remove. - schema: - type: string - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - post: - operationId: PostCustomersCustomerAddressesAddress - summary: Update a Shipping Address - description: Updates a Customer's saved Shipping Address. - parameters: - - in: path - name: id - required: true - description: The Customer id. - schema: - type: string - - in: path - name: address_id - required: true - description: The id of the Address to update. - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - address: - description: The updated Address. - anyOf: - - $ref: '#/components/schemas/address' - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - '/customers/{id}': - get: - operationId: GetCustomersCustomer - summary: Retrieves a Customer - description: >- - Retrieves a Customer - the Customer must be logged in to retrieve their - details. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - post: - operationId: PostCustomersCustomer - summary: Update Customer details - description: Updates a Customer's saved details. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - first_name: - description: The Customer's first name. - type: string - last_name: - description: The Customer's last name. - type: string - billing_address: - description: The Address to be used for billing purposes. - anyOf: - - $ref: '#/components/schemas/address' - password: - description: The Customer's password. - type: string - phone: - description: The Customer's phone number. - type: string - metadata: - description: Metadata about the customer. - type: object - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - '/customers/{id}/payment-methods': - get: - operationId: GetCustomersCustomerPaymentMethods - summary: Retrieve saved payment methods - description: >- - Retrieves a list of a Customer's saved payment methods. Payment methods - are saved with Payment Providers and it is their responsibility to fetch - saved methods. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - payment_methods: - type: array - items: - properties: - provider_id: - type: string - description: >- - The id of the Payment Provider where the payment - method is saved. - data: - type: object - description: >- - The data needed for the Payment Provider to use the - saved payment method. - '/customers/{id}/orders': - get: - operationId: GetCustomersCustomerOrders - summary: Retrieve Customer Orders - description: Retrieves a list of a Customer's Orders. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - payment_methods: - type: array - items: - $ref: '#/components/schemas/order' - '/customers/{id}/password-token': - post: - operationId: PostCustomersCustomerPasswordToken - summary: Creates a reset password token - description: >- - Creates a reset password token to be used in a subsequent - /reset-password request. The password token should be sent out of band - e.g. via email and will not be returned. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - tags: - - Customer - responses: - '204': - description: OK - '/customers/{id}/reset-password': - post: - operationId: PostCustomersCustomerResetPassword - summary: Resets Customer password - description: >- - Resets a Customer's password using a password token created by a - previous /password-token request. - parameters: - - in: path - name: id - required: true - description: The id of the Customer. - schema: - type: string - tags: - - Customer - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - customer: - $ref: '#/components/schemas/customer' - requestBody: - content: - application/json: - schema: - type: object - required: - - email - - token - - password - properties: - email: - type: string - description: The Customer's email. - token: - type: string - description: The password token created by a /password-token request. - password: - type: string - description: The new password to set for the Customer. /auth: post: operationId: PostAuth @@ -1326,6 +1326,66 @@ paths: type: array items: $ref: '#/components/schemas/shipping_option' + /swaps: + post: + operationId: PostSwaps + summary: Create a Swap + description: >- + Creates a Swap on an Order by providing some items to return along with + some items to send back + requestBody: + content: + application/json: + schema: + properties: + order_id: + type: string + description: The id of the Order to create the Swap for. + return_items: + description: The items to include in the Return. + type: array + items: + properties: + item_id: + description: The id of the Line Item from the Order. + type: string + quantity: + description: The quantity to return. + type: integer + return_shipping: + description: >- + If the Return is to be handled by the store operator the + Customer can choose a Return Shipping Method. Alternatvely + the Customer can handle the Return themselves. + type: object + properties: + option_id: + type: string + description: >- + The id of the Shipping Option to create the Shipping + Method from. + additional_items: + description: The items to exchange the returned items to. + type: array + items: + properties: + variant_id: + description: The id of the Product Variant to send. + type: string + quantity: + description: The quantity to send of the variant. + type: integer + tags: + - Swap + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + swap: + $ref: '#/components/schemas/swap' '/swaps/{cart_id}': get: operationId: GetSwapsSwapCartId diff --git a/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap b/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap new file mode 100644 index 0000000000..1fbf422ef2 --- /dev/null +++ b/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap @@ -0,0 +1,365 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`/store/carts /store/swaps simple swap 1`] = ` +Object { + "swap": Object { + "additional_items": Array [ + Object { + "allow_discounts": true, + "cart_id": StringMatching /\\^cart_\\*/, + "claim_order_id": null, + "created_at": Any, + "description": "Swap product", + "fulfilled_quantity": null, + "has_shipping": null, + "id": StringMatching /\\^item_\\*/, + "is_giftcard": false, + "metadata": Object {}, + "order_id": null, + "quantity": 1, + "returned_quantity": null, + "shipped_quantity": null, + "should_merge": true, + "swap_id": StringMatching /\\^swap_\\*/, + "thumbnail": null, + "title": "test product", + "unit_price": 8000, + "updated_at": Any, + "variant": Object { + "allow_backorder": false, + "barcode": null, + "created_at": Any, + "deleted_at": null, + "ean": null, + "height": null, + "hs_code": null, + "id": "test-variant-2", + "inventory_quantity": 1, + "length": null, + "manage_inventory": true, + "material": null, + "metadata": null, + "mid_code": null, + "origin_country": null, + "product": Object { + "collection_id": null, + "created_at": Any, + "deleted_at": null, + "description": null, + "discountable": true, + "handle": null, + "height": null, + "hs_code": null, + "id": "test-product", + "is_giftcard": false, + "length": null, + "material": null, + "metadata": null, + "mid_code": null, + "origin_country": null, + "profile_id": StringMatching /\\^sp_\\*/, + "subtitle": null, + "thumbnail": null, + "title": "test product", + "type_id": null, + "updated_at": Any, + "weight": null, + "width": null, + }, + "product_id": "test-product", + "sku": null, + "title": "Swap product", + "upc": null, + "updated_at": Any, + "weight": null, + "width": null, + }, + "variant_id": "test-variant-2", + }, + ], + "cart": Object { + "billing_address_id": "test-billing-address", + "completed_at": null, + "context": null, + "created_at": Any, + "customer_id": "test-customer", + "deleted_at": null, + "email": "test@email.com", + "id": StringMatching /\\^cart_\\*/, + "idempotency_key": null, + "metadata": Object { + "parent_order_id": "test-order", + "swap_id": StringMatching /\\^swap_\\*/, + }, + "payment_id": null, + "region_id": "test-region", + "shipping_address_id": "test-shipping-address", + "type": "swap", + "updated_at": Any, + }, + "cart_id": StringMatching /\\^cart_\\*/, + "confirmed_at": null, + "created_at": Any, + "deleted_at": null, + "difference_due": null, + "fulfillment_status": "not_fulfilled", + "fulfillments": Array [], + "id": StringMatching /\\^swap_\\*/, + "idempotency_key": Any, + "metadata": null, + "order": Object { + "billing_address_id": "test-billing-address", + "canceled_at": null, + "cart_id": null, + "created_at": Any, + "currency_code": "usd", + "customer_id": "test-customer", + "display_id": 1, + "draft_order_id": null, + "email": "test@email.com", + "fulfillment_status": "fulfilled", + "id": "test-order", + "idempotency_key": null, + "metadata": null, + "no_notification": null, + "payment_status": "captured", + "region_id": "test-region", + "shipping_address_id": "test-shipping-address", + "status": "pending", + "tax_rate": 0, + "updated_at": Any, + }, + "order_id": "test-order", + "payment": null, + "payment_status": "not_paid", + "return_order": Object { + "claim_order_id": null, + "created_at": Any, + "id": StringMatching /\\^ret_\\*/, + "idempotency_key": null, + "items": Array [ + Object { + "is_requested": true, + "item_id": "test-item", + "metadata": null, + "note": null, + "quantity": 1, + "reason_id": null, + "received_quantity": null, + "requested_quantity": 1, + "return_id": StringMatching /\\^ret_\\*/, + }, + ], + "metadata": null, + "no_notification": true, + "order_id": null, + "received_at": null, + "refund_amount": 7200, + "shipping_data": null, + "shipping_method": null, + "status": "requested", + "swap_id": StringMatching /\\^swap_\\*/, + "updated_at": Any, + }, + "shipping_address": null, + "shipping_address_id": null, + "shipping_methods": Array [], + "updated_at": Any, + }, +} +`; + +exports[`/store/carts /store/swaps swap with return shipping 1`] = ` +Object { + "swap": Object { + "additional_items": Array [ + Object { + "allow_discounts": true, + "cart_id": StringMatching /\\^cart_\\*/, + "claim_order_id": null, + "created_at": Any, + "description": "Swap product", + "fulfilled_quantity": null, + "has_shipping": null, + "id": StringMatching /\\^item_\\*/, + "is_giftcard": false, + "metadata": Object {}, + "order_id": null, + "quantity": 1, + "returned_quantity": null, + "shipped_quantity": null, + "should_merge": true, + "swap_id": StringMatching /\\^swap_\\*/, + "thumbnail": null, + "title": "test product", + "unit_price": 8000, + "updated_at": Any, + "variant": Object { + "allow_backorder": false, + "barcode": null, + "created_at": Any, + "deleted_at": null, + "ean": null, + "height": null, + "hs_code": null, + "id": "test-variant-2", + "inventory_quantity": 1, + "length": null, + "manage_inventory": true, + "material": null, + "metadata": null, + "mid_code": null, + "origin_country": null, + "product": Object { + "collection_id": null, + "created_at": Any, + "deleted_at": null, + "description": null, + "discountable": true, + "handle": null, + "height": null, + "hs_code": null, + "id": "test-product", + "is_giftcard": false, + "length": null, + "material": null, + "metadata": null, + "mid_code": null, + "origin_country": null, + "profile_id": StringMatching /\\^sp_\\*/, + "subtitle": null, + "thumbnail": null, + "title": "test product", + "type_id": null, + "updated_at": Any, + "weight": null, + "width": null, + }, + "product_id": "test-product", + "sku": null, + "title": "Swap product", + "upc": null, + "updated_at": Any, + "weight": null, + "width": null, + }, + "variant_id": "test-variant-2", + }, + ], + "cart": Object { + "billing_address_id": "test-billing-address", + "completed_at": null, + "context": null, + "created_at": Any, + "customer_id": "test-customer", + "deleted_at": null, + "email": "test@email.com", + "id": StringMatching /\\^cart_\\*/, + "idempotency_key": null, + "metadata": Object { + "parent_order_id": "test-order", + "swap_id": StringMatching /\\^swap_\\*/, + }, + "payment_id": null, + "region_id": "test-region", + "shipping_address_id": "test-shipping-address", + "type": "swap", + "updated_at": Any, + }, + "cart_id": StringMatching /\\^cart_\\*/, + "confirmed_at": null, + "created_at": Any, + "deleted_at": null, + "difference_due": null, + "fulfillment_status": "not_fulfilled", + "fulfillments": Array [], + "id": StringMatching /\\^swap_\\*/, + "idempotency_key": Any, + "metadata": null, + "order": Object { + "billing_address_id": "test-billing-address", + "canceled_at": null, + "cart_id": null, + "created_at": Any, + "currency_code": "usd", + "customer_id": "test-customer", + "display_id": 2, + "draft_order_id": null, + "email": "test@email.com", + "fulfillment_status": "fulfilled", + "id": "test-order", + "idempotency_key": null, + "metadata": null, + "no_notification": null, + "payment_status": "captured", + "region_id": "test-region", + "shipping_address_id": "test-shipping-address", + "status": "pending", + "tax_rate": 0, + "updated_at": Any, + }, + "order_id": "test-order", + "payment": null, + "payment_status": "not_paid", + "return_order": Object { + "claim_order_id": null, + "created_at": Any, + "id": StringMatching /\\^ret_\\*/, + "idempotency_key": null, + "items": Array [ + Object { + "is_requested": true, + "item_id": "test-item", + "metadata": null, + "note": null, + "quantity": 1, + "reason_id": null, + "received_quantity": null, + "requested_quantity": 1, + "return_id": StringMatching /\\^ret_\\*/, + }, + ], + "metadata": null, + "no_notification": true, + "order_id": null, + "received_at": null, + "refund_amount": 6200, + "shipping_data": Object {}, + "shipping_method": Object { + "cart_id": null, + "claim_order_id": null, + "data": Object {}, + "id": StringMatching /\\^sm_\\*/, + "order_id": null, + "price": 1000, + "return_id": StringMatching /\\^ret_\\*/, + "shipping_option": Object { + "admin_only": false, + "amount": 1000, + "created_at": Any, + "data": Object {}, + "deleted_at": null, + "id": "return-option", + "is_return": true, + "metadata": null, + "name": "Test ret", + "price_type": "flat_rate", + "profile_id": StringMatching /\\^sp_\\*/, + "provider_id": "test-ful", + "region_id": "test-region", + "updated_at": Any, + }, + "shipping_option_id": "return-option", + "swap_id": null, + }, + "status": "requested", + "swap_id": StringMatching /\\^swap_\\*/, + "updated_at": Any, + }, + "shipping_address": null, + "shipping_address_id": null, + "shipping_methods": Array [], + "updated_at": Any, + }, +} +`; diff --git a/integration-tests/api/__tests__/store/orders.js b/integration-tests/api/__tests__/store/orders.js index 2a7e9f1071..12112307f9 100644 --- a/integration-tests/api/__tests__/store/orders.js +++ b/integration-tests/api/__tests__/store/orders.js @@ -1,4 +1,4 @@ -const path = require("path"); +const path = require("path") const { Region, Order, @@ -12,110 +12,49 @@ const { Cart, ShippingMethod, Swap, -} = require("@medusajs/medusa"); +} = require("@medusajs/medusa") -const setupServer = require("../../../helpers/setup-server"); -const { useApi } = require("../../../helpers/use-api"); -const { initDb, useDb } = require("../../../helpers/use-db"); +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +const { initDb, useDb } = require("../../../helpers/use-db") -const swapSeeder = require("../../helpers/swap-seeder"); -const cartSeeder = require("../../helpers/cart-seeder"); +const swapSeeder = require("../../helpers/swap-seeder") +const cartSeeder = require("../../helpers/cart-seeder") -jest.setTimeout(30000); +jest.setTimeout(30000) describe("/store/carts", () => { - let medusaProcess; - let dbConnection; + let medusaProcess + let dbConnection beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")); - dbConnection = await initDb({ cwd }); - medusaProcess = await setupServer({ cwd }); - }); + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) afterAll(async () => { - const db = useDb(); - await db.shutdown(); - medusaProcess.kill(); - }); - - describe("/store/swaps", () => { - beforeEach(async () => { - try { - await cartSeeder(dbConnection); - await swapSeeder(dbConnection); - - const manager = dbConnection.manager; - await manager.query( - `UPDATE "swap" SET cart_id='test-cart-2' WHERE id = 'test-swap'` - ); - await manager.query( - `UPDATE "payment" SET swap_id=NULL WHERE id = 'test-payment-swap'` - ); - } catch (err) { - console.log(err); - throw err; - } - }); - - afterEach(async () => { - const db = useDb(); - await db.teardown(); - }); - - it("creates a swap from a cart id", async () => { - const api = useApi(); - - const getRes = await api.post("/store/swaps", { - cart_id: "test-cart-2", - }); - expect(getRes.status).toEqual(200); - }); - - it("fails due to partial inventory", async () => { - const api = useApi(); - const manager = dbConnection.manager; - - const li = manager.create(LineItem, { - id: "test-item-with-no-stock", - title: "No Stock Item", - description: "Line Item Desc", - thumbnail: "https://test.js/1234", - unit_price: 8000, - quantity: 1, - variant_id: "test-variant-2", - cart_id: "test-cart-2", - }); - await manager.save(li); - - try { - await api.post("/store/swaps", { - cart_id: "test-cart-2", - }); - } catch (e) { - expect(e.response.data.message).toEqual( - "Variant with id: test-variant-2 does not have the required inventory" - ); - } - }); - }); + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) describe("GET /store/orders", () => { beforeEach(async () => { - const manager = dbConnection.manager; + const manager = dbConnection.manager await manager.query( `ALTER SEQUENCE order_display_id_seq RESTART WITH 111` - ); + ) await manager.insert(Region, { id: "region", name: "Test Region", currency_code: "usd", tax_rate: 0, - }); + }) await manager.insert(Customer, { id: "cus_1234", email: "test@email.com", - }); + }) await manager.insert(Order, { id: "order_test", email: "test@email.com", @@ -124,17 +63,17 @@ describe("/store/carts", () => { region_id: "region", tax_rate: 0, currency_code: "usd", - }); + }) const defaultProfile = await manager.findOne(ShippingProfile, { type: "default", - }); + }) await manager.insert(Product, { id: "test-product", title: "test product", profile_id: defaultProfile.id, options: [{ id: "test-option", title: "Size" }], - }); + }) await manager.insert(ProductVariant, { id: "test-variant", @@ -147,7 +86,7 @@ describe("/store/carts", () => { value: "Size", }, ], - }); + }) await manager.insert(LineItem, { id: "test-item", @@ -158,59 +97,59 @@ describe("/store/carts", () => { unit_price: 8000, quantity: 1, variant_id: "test-variant", - }); - }); + }) + }) afterEach(async () => { - const db = useDb(); - await db.teardown(); - }); + const db = useDb() + await db.teardown() + }) it("looks up order", async () => { - const api = useApi(); + const api = useApi() const response = await api .get("/store/orders?display_id=111&email=test@email.com") .catch((err) => { - return err.response; - }); - expect(response.status).toEqual(200); - expect(response.data.order.display_id).toEqual(111); - expect(response.data.order.email).toEqual("test@email.com"); - }); + return err.response + }) + expect(response.status).toEqual(200) + expect(response.data.order.display_id).toEqual(111) + expect(response.data.order.email).toEqual("test@email.com") + }) it("fails if display_id + email not provided", async () => { - const api = useApi(); + const api = useApi() const response = await api .get("/store/orders?display_id=111") .catch((err) => { - return err.response; - }); - expect(response.status).toEqual(400); - }); + return err.response + }) + expect(response.status).toEqual(400) + }) it("fails if display_id + email not provided", async () => { - const api = useApi(); + const api = useApi() const response = await api .get("/store/orders?email=test@email.com") .catch((err) => { - return err.response; - }); - expect(response.status).toEqual(400); - }); + return err.response + }) + expect(response.status).toEqual(400) + }) it("fails if email not correct", async () => { - const api = useApi(); + const api = useApi() const response = await api .get("/store/orders?display_id=111&email=test1@email.com") .catch((err) => { - return err.response; - }); + return err.response + }) - expect(response.status).toEqual(404); - }); - }); -}); + expect(response.status).toEqual(404) + }) + }) +}) diff --git a/integration-tests/api/__tests__/store/swaps.js b/integration-tests/api/__tests__/store/swaps.js new file mode 100644 index 0000000000..f7bffc9eb1 --- /dev/null +++ b/integration-tests/api/__tests__/store/swaps.js @@ -0,0 +1,258 @@ +const path = require("path") +const { + Region, + Order, + Customer, + ShippingProfile, + Product, + ProductVariant, + MoneyAmount, + LineItem, + Payment, + Cart, + ShippingMethod, + ShippingOption, + Swap, +} = require("@medusajs/medusa") + +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +const { initDb, useDb } = require("../../../helpers/use-db") + +const orderSeeder = require("../../helpers/order-seeder") + +jest.setTimeout(30000) + +describe("/store/carts", () => { + let medusaProcess + let dbConnection + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) + + describe("/store/swaps", () => { + beforeEach(async () => { + try { + await orderSeeder(dbConnection) + + const manager = dbConnection.manager + await manager.query( + `UPDATE "swap" SET cart_id='test-cart-2' WHERE id = 'test-swap'` + ) + await manager.query( + `UPDATE "payment" SET swap_id=NULL WHERE id = 'test-payment-swap'` + ) + + const defaultProfile = await manager.findOne(ShippingProfile, { + type: "default", + }) + await manager.insert(ShippingOption, { + id: "return-option", + name: "Test ret", + profile_id: defaultProfile.id, + region_id: "test-region", + provider_id: "test-ful", + data: {}, + price_type: "flat_rate", + amount: 1000, + is_return: true, + }) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("simple swap", async () => { + const api = useApi() + + const response = await api + .post("/store/swaps", { + order_id: "test-order", + return_items: [ + { + item_id: "test-item", + quantity: 1, + }, + ], + additional_items: [ + { + variant_id: "test-variant-2", + quantity: 1, + }, + ], + }) + .catch((err) => { + console.log(err.response.data.message) + }) + + expect(response.data).toMatchSnapshot({ + swap: { + id: expect.stringMatching(/^swap_*/), + idempotency_key: expect.any(String), + additional_items: [ + { + id: expect.stringMatching(/^item_*/), + cart_id: expect.stringMatching(/^cart_*/), + swap_id: expect.stringMatching(/^swap_*/), + variant: { + id: "test-variant-2", + created_at: expect.any(String), + updated_at: expect.any(String), + product: { + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }, + quantity: 1, + variant_id: "test-variant-2", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + order: { + id: "test-order", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + cart_id: expect.stringMatching(/^cart_*/), + cart: { + id: expect.stringMatching(/^cart_*/), + billing_address_id: "test-billing-address", + type: "swap", + created_at: expect.any(String), + updated_at: expect.any(String), + metadata: { + swap_id: expect.stringMatching(/^swap_*/), + }, + }, + return_order: { + id: expect.stringMatching(/^ret_*/), + swap_id: expect.stringMatching(/^swap_*/), + refund_amount: 7200, + items: [ + { + item_id: "test-item", + quantity: 1, + return_id: expect.stringMatching(/^ret_*/), + }, + ], + created_at: expect.any(String), + updated_at: expect.any(String), + }, + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }) + }) + + it("swap with return shipping", async () => { + const api = useApi() + + const response = await api + .post("/store/swaps", { + order_id: "test-order", + return_items: [ + { + item_id: "test-item", + quantity: 1, + }, + ], + return_shipping_option: "return-option", + additional_items: [ + { + variant_id: "test-variant-2", + quantity: 1, + }, + ], + }) + .catch((err) => { + console.log(err.response.data.message) + }) + + expect(response.data).toMatchSnapshot({ + swap: { + id: expect.stringMatching(/^swap_*/), + idempotency_key: expect.any(String), + additional_items: [ + { + id: expect.stringMatching(/^item_*/), + cart_id: expect.stringMatching(/^cart_*/), + swap_id: expect.stringMatching(/^swap_*/), + variant: { + id: "test-variant-2", + created_at: expect.any(String), + updated_at: expect.any(String), + product: { + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }, + quantity: 1, + variant_id: "test-variant-2", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + order: { + id: "test-order", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + cart_id: expect.stringMatching(/^cart_*/), + cart: { + id: expect.stringMatching(/^cart_*/), + billing_address_id: "test-billing-address", + type: "swap", + created_at: expect.any(String), + updated_at: expect.any(String), + metadata: { + swap_id: expect.stringMatching(/^swap_*/), + }, + }, + return_order: { + id: expect.stringMatching(/^ret_*/), + swap_id: expect.stringMatching(/^swap_*/), + refund_amount: 6200, + shipping_method: { + id: expect.stringMatching(/^sm_*/), + return_id: expect.stringMatching(/^ret_*/), + shipping_option: { + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }, + items: [ + { + item_id: "test-item", + quantity: 1, + return_id: expect.stringMatching(/^ret_*/), + }, + ], + created_at: expect.any(String), + updated_at: expect.any(String), + }, + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }) + }) + }) +}) diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index 742e683fd5..49bce3d034 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,15 +8,15 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.1.36-dev-1629109473927", - "medusa-interfaces": "1.1.21-dev-1629109473927", + "@medusajs/medusa": "1.1.38-dev-1630001256218", + "medusa-interfaces": "1.1.21-dev-1630001256218", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.13-dev-1629109473927", + "babel-preset-medusa-package": "1.1.13-dev-1630001256218", "jest": "^26.6.3" } } diff --git a/integration-tests/api/setup.js b/integration-tests/api/setup.js deleted file mode 100644 index 861746aaaa..0000000000 --- a/integration-tests/api/setup.js +++ /dev/null @@ -1,5 +0,0 @@ -const { dropDatabase } = require("pg-god"); - -afterAll(() => { - dropDatabase({ databaseName: "medusa-integration" }); -}); diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index b1e9576e06..6cc1132371 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1230,10 +1230,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@medusajs/medusa-cli@1.1.15-dev-1629109473927": - version "1.1.15-dev-1629109473927" - resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.15-dev-1629109473927.tgz#d7d170daab4fc471f78841d55799f8d0c426c3a9" - integrity sha512-VOaMDt1vkB/13dXqr9eeW6fKspI5sOzh1hAMZvJZ8Y3k0+2/eHr7imzxwBwOHW9qbgzCASq31hNGRl5SG+GigQ== +"@medusajs/medusa-cli@1.1.16-dev-1630001256218": + version "1.1.16-dev-1630001256218" + resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.16-dev-1630001256218.tgz#f1df1f0cb89e56261690640e7743c5cfa34b70d9" + integrity sha512-02owiukP3bsGSo5yhuVUpNEB++CpkaJVDAxR7n8XxkIj+Jh4lBnTrO1m4JU4IddZZzHvm/ikpw/hDSgUuWia+g== dependencies: "@babel/polyfill" "^7.8.7" "@babel/runtime" "^7.9.6" @@ -1251,8 +1251,8 @@ is-valid-path "^0.1.1" joi-objectid "^3.0.1" meant "^1.0.1" - medusa-core-utils "1.1.20-dev-1629109473927" - medusa-telemetry "0.0.2-dev-1629109473927" + medusa-core-utils "1.1.20-dev-1630001256218" + medusa-telemetry "0.0.3-dev-1630001256218" netrc-parser "^3.1.6" open "^8.0.6" ora "^5.4.1" @@ -1266,13 +1266,13 @@ winston "^3.3.3" yargs "^15.3.1" -"@medusajs/medusa@1.1.36-dev-1629109473927": - version "1.1.36-dev-1629109473927" - resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.36-dev-1629109473927.tgz#198c64c177aa5d39785fbada7f97bfc54d41737b" - integrity sha512-4URRvrzBFqU72xGy0jZw3puLknNlZLQ2qCSopMF36qqPa0EFfZhC/fu2x7DIqAdbR8+WxPV/IV8nJQZwlbcOAA== +"@medusajs/medusa@1.1.38-dev-1630001256218": + version "1.1.38-dev-1630001256218" + resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.38-dev-1630001256218.tgz#f01c1644c4f9ee741d1effbbc9458c9bdcc75a10" + integrity sha512-WdJDV3GP642uUaoxg+65zUXmc3LMKqzctSjNX3iegpjfcB4fat9TXXiO00+YEV95nyRfxrhixMzmH7If7XY1jA== dependencies: "@hapi/joi" "^16.1.8" - "@medusajs/medusa-cli" "1.1.15-dev-1629109473927" + "@medusajs/medusa-cli" "1.1.16-dev-1630001256218" "@types/lodash" "^4.14.168" awilix "^4.2.3" body-parser "^1.19.0" @@ -1282,6 +1282,7 @@ cookie-parser "^1.4.4" core-js "^3.6.5" cors "^2.8.5" + cross-spawn "^7.0.3" dotenv "^8.2.0" express "^4.17.1" express-session "^1.17.1" @@ -1292,8 +1293,8 @@ joi "^17.3.0" joi-objectid "^3.0.1" jsonwebtoken "^8.5.1" - medusa-core-utils "1.1.20-dev-1629109473927" - medusa-test-utils "1.1.23-dev-1629109473927" + medusa-core-utils "1.1.20-dev-1630001256218" + medusa-test-utils "1.1.23-dev-1630001256218" morgan "^1.9.1" multer "^1.4.2" passport "^0.4.0" @@ -1938,10 +1939,10 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-preset-medusa-package@1.1.13-dev-1629109473927: - version "1.1.13-dev-1629109473927" - resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.13-dev-1629109473927.tgz#1ec273faf2a64f1a02a23106de19d2babc9223bc" - integrity sha512-Im7BInUm9S0OTb04CLiWRUNpVdNciJ7xVwFicNvCettujV5DvJWpMK/pYhYj+5HCyvYkZcgR2X3KZTpOtIt+uA== +babel-preset-medusa-package@1.1.13-dev-1630001256218: + version "1.1.13-dev-1630001256218" + resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.13-dev-1630001256218.tgz#6a040844854c36aa054c63192dfff3eb96ea1175" + integrity sha512-9MhOVXbNcMnHKg7FxMaQ81dswTS9eP8ow44OC4RoMa8xKxV9DCTcD3H8asJSYhP7k5EqkCP9EvT+6CUspAt6wQ== dependencies: "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-decorators" "^7.12.1" @@ -5073,25 +5074,25 @@ 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.1.20-dev-1629109473927: - version "1.1.20-dev-1629109473927" - resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.20-dev-1629109473927.tgz#fec5497a8d06cc4c264755fc691dcc89c285d041" - integrity sha512-qAdf4KZRDtB4hPI5TYTCTlGXP+0WAbNOm5BX77aiLBGhDqjcYTH6zE2E1KWnMcpeydtqoVQ5Cpnt52tPX/HAFw== +medusa-core-utils@1.1.20-dev-1630001256218: + version "1.1.20-dev-1630001256218" + resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.20-dev-1630001256218.tgz#0a87f9580ed5afec881eca71b31c47233bdbd527" + integrity sha512-tOblNeS7/ko3Q6cKwqwe0hAj/QL9cu8568Cx4dArQv4in1MIqSfnP3qZ6lwH/gvYLGhEJi4F70A2bYOoAqxNIw== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" -medusa-interfaces@1.1.21-dev-1629109473927: - version "1.1.21-dev-1629109473927" - resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.21-dev-1629109473927.tgz#b53f49b31a6ff34de09a4b2a71e10966aff4210d" - integrity sha512-gtxUAcWD5yyWTMnHm7egamyVKtgc2bbaHfF/ezK7y+dqUPwHI9nry4Kpuy7YboF5XX4X/FHVajkRfGSJVPun7Q== +medusa-interfaces@1.1.21-dev-1630001256218: + version "1.1.21-dev-1630001256218" + resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.21-dev-1630001256218.tgz#0b1b509e0db91b0299cf9f2cc8034aaeeb5bb2b6" + integrity sha512-tmTTagdicUxImDR4V2fvU5j2johMvVdL71ptFcJJFxYUSZarRz6KcfPaQZzjVK+eJwIVG9qFjNVVoR/xk7sb4Q== dependencies: - medusa-core-utils "1.1.20-dev-1629109473927" + medusa-core-utils "1.1.20-dev-1630001256218" -medusa-telemetry@0.0.2-dev-1629109473927: - version "0.0.2-dev-1629109473927" - resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.2-dev-1629109473927.tgz#4ac0eb3ef4d1d0dea112a841023c2415b09fd5df" - integrity sha512-a+k7QtRx9o3AFHsp7i9hijSTT5KWeDPKNFkRIcB3gEMaS+CkcCZ3CA3563yoeRJmGKpAcSxrRtbFDz8v757dLA== +medusa-telemetry@0.0.3-dev-1630001256218: + version "0.0.3-dev-1630001256218" + resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.3-dev-1630001256218.tgz#d3a43c9d10ee32a189f236cd9082aee62606948b" + integrity sha512-8RhUNhTvu6NsqBZZuoWeNQE5bPVgoMeBdzOlr9RnY4vfxjUlzLM7mNanRIol06Zmhz9sRd8/8Tqe2HfcfrSd6A== dependencies: axios "^0.21.1" axios-retry "^3.1.9" @@ -5102,13 +5103,13 @@ medusa-telemetry@0.0.2-dev-1629109473927: remove-trailing-slash "^0.1.1" uuid "^8.3.2" -medusa-test-utils@1.1.23-dev-1629109473927: - version "1.1.23-dev-1629109473927" - resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.23-dev-1629109473927.tgz#b7e47941f48b0a0b8be8d11b4e1dac75a1ada71e" - integrity sha512-6NE+XQXBc6Qo8hFxCpY+mTzjOdymKlzdIokG8C+FihK9vLx79xI2iCKD9vv7ERmUA6j5ebNohS3GfRyPwfdMaQ== +medusa-test-utils@1.1.23-dev-1630001256218: + version "1.1.23-dev-1630001256218" + resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.23-dev-1630001256218.tgz#8c0f4db11ee345b40ea51120e26df64f59b0ce9c" + integrity sha512-jZH8wfxKtgJDJ7psdJ9ADmhbvtHTFKgrTohDrJ3HHu7NpUMAstA7NXc9v2kKCbznpQae81BI3lW/12nCfqel8Q== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "1.1.20-dev-1629109473927" + medusa-core-utils "1.1.20-dev-1630001256218" randomatic "^3.1.1" merge-descriptors@1.0.1: diff --git a/integration-tests/helpers/setup-server.js b/integration-tests/helpers/setup-server.js index 053ed4f5fb..f10c772e5c 100644 --- a/integration-tests/helpers/setup-server.js +++ b/integration-tests/helpers/setup-server.js @@ -1,10 +1,10 @@ -const path = require("path"); -const { spawn } = require("child_process"); +const path = require("path") +const { spawn } = require("child_process") -const { setPort } = require("./use-api"); +const { setPort } = require("./use-api") module.exports = ({ cwd, verbose }) => { - const serverPath = path.join(__dirname, "test-server.js"); + const serverPath = path.join(__dirname, "test-server.js") return new Promise((resolve, reject) => { const medusaProcess = spawn("node", [path.resolve(serverPath)], { @@ -18,15 +18,15 @@ module.exports = ({ cwd, verbose }) => { stdio: verbose ? ["inherit", "inherit", "inherit", "ipc"] : ["ignore", "ignore", "ignore", "ipc"], - }); + }) medusaProcess.on("uncaughtException", (err) => { - medusaProcess.kill(); - }); + medusaProcess.kill() + }) medusaProcess.on("message", (port) => { - setPort(port); - resolve(medusaProcess); - }); - }); -}; + setPort(port) + resolve(medusaProcess) + }) + }) +} diff --git a/integration-tests/helpers/test-server.js b/integration-tests/helpers/test-server.js index 1ce58decd7..a06bc12ab8 100644 --- a/integration-tests/helpers/test-server.js +++ b/integration-tests/helpers/test-server.js @@ -1,34 +1,34 @@ -const path = require("path"); -const express = require("express"); -const getPort = require("get-port"); -const importFrom = require("import-from"); +const path = require("path") +const express = require("express") +const getPort = require("get-port") +const importFrom = require("import-from") const initialize = async () => { - const app = express(); + const app = express() const loaders = importFrom(process.cwd(), "@medusajs/medusa/dist/loaders") - .default; + .default const { dbConnection } = await loaders({ directory: path.resolve(process.cwd()), expressApp: app, - }); + }) - const PORT = await getPort(); + const PORT = await getPort() return { db: dbConnection, app, port: PORT, - }; -}; + } +} const setup = async () => { - const { app, port } = await initialize(); + const { app, port } = await initialize() app.listen(port, (err) => { - process.send(port); - }); -}; + process.send(port) + }) +} -setup(); +setup() diff --git a/integration-tests/helpers/use-api.js b/integration-tests/helpers/use-api.js index 125e557e23..e2cc75987a 100644 --- a/integration-tests/helpers/use-api.js +++ b/integration-tests/helpers/use-api.js @@ -1,21 +1,21 @@ -const axios = require("axios").default; +const axios = require("axios").default const ServerTestUtil = { port_: null, client_: null, setPort: function (port) { - this.client_ = axios.create({ baseURL: `http://localhost:${port}` }); + this.client_ = axios.create({ baseURL: `http://localhost:${port}` }) }, -}; +} -const instance = ServerTestUtil; +const instance = ServerTestUtil module.exports = { setPort: function (port) { - instance.setPort(port); + instance.setPort(port) }, useApi: function () { - return instance.client_; + return instance.client_ }, -}; +} diff --git a/integration-tests/helpers/use-db.js b/integration-tests/helpers/use-db.js index 18cd785a17..5526034755 100644 --- a/integration-tests/helpers/use-db.js +++ b/integration-tests/helpers/use-db.js @@ -1,17 +1,17 @@ -const path = require("path"); -require("dotenv").config({ path: path.join(__dirname, "../.env") }); +const path = require("path") +require("dotenv").config({ path: path.join(__dirname, "../.env") }) -const { dropDatabase, createDatabase } = require("pg-god"); -const { createConnection } = require("typeorm"); +const { dropDatabase, createDatabase } = require("pg-god") +const { createConnection } = require("typeorm") -const DB_USERNAME = process.env.DB_USERNAME || "postgres"; -const DB_PASSWORD = process.env.DB_PASSWORD || ""; -const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost/medusa-integration`; +const DB_USERNAME = process.env.DB_USERNAME || "postgres" +const DB_PASSWORD = process.env.DB_PASSWORD || "" +const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost/medusa-integration` const pgGodCredentials = { user: DB_USERNAME, password: DB_PASSWORD, -}; +} const keepTables = [ "staged_job", @@ -20,57 +20,57 @@ const keepTables = [ "payment_provider", "country", "currency", -]; +] -let connectionType = "postgresql"; +let connectionType = "postgresql" const DbTestUtil = { db_: null, setDb: function (connection) { - this.db_ = connection; + this.db_ = connection }, clear: async function () { - this.db_.synchronize(true); + this.db_.synchronize(true) }, teardown: async function () { - const entities = this.db_.entityMetadatas; - const manager = this.db_.manager; + const entities = this.db_.entityMetadatas + const manager = this.db_.manager if (connectionType === "sqlite") { - await manager.query(`PRAGMA foreign_keys = OFF`); + await manager.query(`PRAGMA foreign_keys = OFF`) } else { - await manager.query(`SET session_replication_role = 'replica';`); + await manager.query(`SET session_replication_role = 'replica';`) } for (const entity of entities) { if (keepTables.includes(entity.tableName)) { - continue; + continue } - await manager.query(`DELETE FROM "${entity.tableName}";`); + await manager.query(`DELETE FROM "${entity.tableName}";`) } if (connectionType === "sqlite") { - await manager.query(`PRAGMA foreign_keys = ON`); + await manager.query(`PRAGMA foreign_keys = ON`) } else { - await manager.query(`SET session_replication_role = 'origin';`); + await manager.query(`SET session_replication_role = 'origin';`) } }, shutdown: async function () { - await this.db_.close(); - const databaseName = "medusa-integration"; - return await dropDatabase({ databaseName }, pgGodCredentials); + await this.db_.close() + const databaseName = "medusa-integration" + return await dropDatabase({ databaseName }, pgGodCredentials) }, -}; +} -const instance = DbTestUtil; +const instance = DbTestUtil module.exports = { initDb: async function ({ cwd }) { - const configPath = path.resolve(path.join(cwd, `medusa-config.js`)); + const configPath = path.resolve(path.join(cwd, `medusa-config.js`)) const modelsLoader = require(path.join( cwd, @@ -80,21 +80,21 @@ module.exports = { `dist`, `loaders`, `models` - )).default; - const entities = modelsLoader({}, { register: false }); + )).default + const entities = modelsLoader({}, { register: false }) - const { projectConfig } = require(configPath); + const { projectConfig } = require(configPath) if (projectConfig.database_type === "sqlite") { - connectionType = "sqlite"; + connectionType = "sqlite" const dbConnection = await createConnection({ type: "sqlite", database: projectConfig.database_database, synchronize: true, entities, - }); + }) - instance.setDb(dbConnection); - return dbConnection; + instance.setDb(dbConnection) + return dbConnection } else { const migrationDir = path.resolve( path.join( @@ -105,31 +105,31 @@ module.exports = { `dist`, `migrations` ) - ); + ) - const databaseName = "medusa-integration"; - await createDatabase({ databaseName }, pgGodCredentials); + const databaseName = "medusa-integration" + await createDatabase({ databaseName }, pgGodCredentials) const connection = await createConnection({ type: "postgres", url: DB_URL, migrations: [`${migrationDir}/*.js`], - }); + }) - await connection.runMigrations(); - await connection.close(); + await connection.runMigrations() + await connection.close() const dbConnection = await createConnection({ type: "postgres", url: DB_URL, entities, - }); + }) - instance.setDb(dbConnection); - return dbConnection; + instance.setDb(dbConnection) + return dbConnection } }, useDb: function () { - return instance; + return instance }, -}; +} diff --git a/integration-tests/helpers/use-server.js b/integration-tests/helpers/use-server.js index 14856511cb..cfc324cbab 100644 --- a/integration-tests/helpers/use-server.js +++ b/integration-tests/helpers/use-server.js @@ -3,39 +3,39 @@ const ServerTestUtil = { app_: null, setApp: function (app) { - this.app_ = app; + this.app_ = app }, start: async function () { this.server_ = await new Promise((resolve, reject) => { const s = this.app_.listen(PORT, (err) => { if (err) { - reject(err); + reject(err) } - }); - resolve(s); - }); + }) + resolve(s) + }) }, kill: function () { return new Promise((resolve, _) => { if (this.server_) { - this.server_.close(() => resolve()); + this.server_.close(() => resolve()) } - resolve(); - }); + resolve() + }) }, -}; +} -const instance = ServerTestUtil; +const instance = ServerTestUtil module.exports = { setApp: function (app) { - instance.setApp(app); - return instance; + instance.setApp(app) + return instance }, useServer: function () { - return instance; + return instance }, -}; +} diff --git a/integration-tests/setup.js b/integration-tests/setup.js index 861746aaaa..497b44047e 100644 --- a/integration-tests/setup.js +++ b/integration-tests/setup.js @@ -1,5 +1,16 @@ -const { dropDatabase } = require("pg-god"); +const path = require('path'); +const {dropDatabase} = require('pg-god'); + +require('dotenv').config({path: path.join(__dirname, '.env')}); + +const DB_USERNAME = process.env.DB_USERNAME || 'postgres'; +const DB_PASSWORD = process.env.DB_PASSWORD || ''; + +const pgGodCredentials = { + user: DB_USERNAME, + password: DB_PASSWORD, +}; afterAll(() => { - dropDatabase({ databaseName: "medusa-integration" }); + dropDatabase({databaseName: 'medusa-integration'}, pgGodCredentials); }); 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 370f96f65f..eb115ca443 100644 --- a/packages/medusa/src/api/routes/store/swaps/create-swap.js +++ b/packages/medusa/src/api/routes/store/swaps/create-swap.js @@ -1,8 +1,73 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" + +/** + * @oas [post] /swaps + * operationId: PostSwaps + * summary: Create a Swap + * description: "Creates a Swap on an Order by providing some items to return along with some items to send back" + * requestBody: + * content: + * application/json: + * schema: + * properties: + * order_id: + * type: string + * description: The id of the Order to create the Swap for. + * return_items: + * description: "The items to include in the Return." + * type: array + * items: + * properties: + * item_id: + * description: The id of the Line Item from the Order. + * type: string + * quantity: + * description: The quantity to return. + * type: integer + * return_shipping_option: + * type: string + * description: The id of the Shipping Option to create the Shipping Method from. + * additional_items: + * description: "The items to exchange the returned items to." + * type: array + * items: + * properties: + * variant_id: + * description: The id of the Product Variant to send. + * type: string + * quantity: + * description: The quantity to send of the variant. + * type: integer + * tags: + * - Swap + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * swap: + * $ref: "#/components/schemas/swap" + */ export default async (req, res) => { const schema = Validator.object().keys({ - cart_id: Validator.string().required(), + order_id: Validator.string().required(), + return_items: Validator.array() + .items({ + item_id: Validator.string().required(), + quantity: Validator.number().required(), + reason_id: Validator.string().optional(), + note: Validator.string().optional(), + }) + .required(), + return_shipping_option: Validator.string().optional(), + additional_items: Validator.array().items({ + variant_id: Validator.string().required(), + quantity: Validator.number().required(), + }), }) const { value, error } = schema.validate(req.body) @@ -10,17 +75,151 @@ export default async (req, res) => { throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) } - const swapService = req.scope.resolve("swapService") + 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 swap = await swapService.retrieveByCartId(value.cart_id) - const data = await swapService.registerCartCompletion( - swap.id, - value.cart_id - ) + const orderService = req.scope.resolve("orderService") + const swapService = req.scope.resolve("swapService") + const returnService = req.scope.resolve("returnService") - res.status(200).json({ swap: data }) - } catch (err) { - throw err + 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 order = await orderService + .withTransaction(manager) + .retrieve(value.order_id, { + select: ["refunded_total", "total"], + relations: ["items", "swaps", "swaps.additional_items"], + }) + + let returnShipping + if (value.return_shipping_option) { + returnShipping = { + option_id: value.return_shipping_option, + } + } + + const swap = await swapService + .withTransaction(manager) + .create( + order, + value.return_items, + value.additional_items, + returnShipping, + { + idempotency_key: idempotencyKey.idempotency_key, + no_notification: true, + } + ) + + await swapService.withTransaction(manager).createCart(swap.id) + const returnOrder = await returnService + .withTransaction(manager) + .retrieveBySwap(swap.id) + + await returnService + .withTransaction(manager) + .fulfill(returnOrder.id) + + return { + recovery_point: "swap_created", + } + } + ) + + if (error) { + inProgress = false + err = error + } else { + idempotencyKey = key + } + break + } + + 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 swap = await swapService.retrieve(swaps[0].id, { + select: defaultFields, + relations: defaultRelations, + }) + + return { + response_code: 200, + response_body: { swap }, + } + } + ) + + 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/store/swaps/index.js b/packages/medusa/src/api/routes/store/swaps/index.js index 944f60aa35..605b967d20 100644 --- a/packages/medusa/src/api/routes/store/swaps/index.js +++ b/packages/medusa/src/api/routes/store/swaps/index.js @@ -3,7 +3,7 @@ import middlewares from "../../../middlewares" const route = Router() -export default app => { +export default (app) => { app.use("/swaps", route) route.get( @@ -14,3 +14,29 @@ export default app => { return app } + +export const defaultRelations = [ + "order", + "additional_items", + "return_order", + "fulfillments", + "payment", + "shipping_address", + "shipping_methods", + "cart", +] +export const defaultFields = [ + "id", + "fulfillment_status", + "payment_status", + "order_id", + "difference_due", + "shipping_address_id", + "cart_id", + "confirmed_at", + "created_at", + "updated_at", + "deleted_at", + "metadata", + "idempotency_key", +]