diff --git a/graphql/schema.graphql b/graphql/schema.graphql index a99d4aafb..45698121e 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -42,4 +42,5 @@ type Mutation { comment: ReturnRequestCommentInput refundData: RefundDataInput ): ReturnRequestResponse @withUserProfile + sendReturnLabel(requestId: String!, labelUrl: String!): Boolean } diff --git a/graphql/types/ReturnRequest.graphql b/graphql/types/ReturnRequest.graphql index 7ae4911d0..9025c5a82 100644 --- a/graphql/types/ReturnRequest.graphql +++ b/graphql/types/ReturnRequest.graphql @@ -45,6 +45,7 @@ input PickupReturnDataInput { country: String! zipCode: String! addressType: AddressType! + labelUrl: String! } enum AddressType { @@ -110,7 +111,7 @@ type PickupReturnData { country: String! zipCode: String! addressType: AddressType! - returnLabel: String + labelUrl: String } type RefundPaymentData { diff --git a/manifest.json b/manifest.json index ebe535ad2..d9b4bfed5 100644 --- a/manifest.json +++ b/manifest.json @@ -14,7 +14,8 @@ "vtex.css-handles": "0.x", "vtex.easypost": "0.x", "vtex.tenant-graphql": "0.x", - "vtex.catalog-graphql": "1.x" + "vtex.catalog-graphql": "1.x", + "vtex.apps-graphql": "3.x" }, "builders": { "admin": "0.x", diff --git a/messages/context.json b/messages/context.json index 428dda41d..082f12303 100644 --- a/messages/context.json +++ b/messages/context.json @@ -308,6 +308,7 @@ "return-app.return-request-details.order-id.link": "Order {orderId}", "return-app.return-request-details.current-status.request-id": "Request id: {id}", "return-app.return-request-details.current-status.status": "Status:", + "return-app.return-request-details.current-status.see-return-label": "See return label", "admin/return-app.return-request-list.table-data.requestId.tooltip": "Tooltip with an explanation of how the customer can access the data", "admin/return-app.settings.modal-warning.title": "Title of a modal to warn about max days settings", "admin/return-app.settings.modal-warning.first-paragraph": "First parahraph describing the warning", @@ -320,6 +321,13 @@ "store/return-app.return-order-details.pickup-address.drop-off-points.dropdown.placehoder": "Select a drop off point", "store/return-app.return-order-details.pickup-address.drop-off-points.dropdown.error": "There was an error loading the drop off points", "admin/return-app.return-request-details.localized-product-name.tooltip": "Tooltip for localized product names", + "admin/return-app.return-request-details.return-label.modal-title": "Are you sure you want to create a return shipping label?", + "admin/return-app.return-request-details.return-label.modal-message": "This action is irreversible. The created label will be sent via email to the customer.", + "admin/return-app.return-request-details.return-label.create-return-label": "Create return label", + "admin/return-app.return-request-details.return-label.see-return-label": "See return label", + "admin/return-app.return-request-details.return-label-info.tooltip": "Create a return shipping label and send it to the customer via email. The request status has to be processing. For this integration to work properly you must install and configure vtex.easypost.", + "admin/return-app.return-request-details.return-label.alert.success": "The return label was created successfully", + "admin/return-app.return-request-details.return-label.alert.error": "There was an error creating the return label, please try again.", "admin/return-app.settings.section.payment-options.refund-method-strategy.checkbox.label": "Automatically refund requests", "admin/return-app.settings.section.payment-options.refund-method-strategy.checkbox.description": "Automatically create the return invoice in the OMS for the requests", "return-app.return-request-details.payent-method.refund-option.refund-process": "Refund process: {automaticallyRefundPaymentMethod, select, true{Automatically} false{Manually}}", diff --git a/messages/en.json b/messages/en.json index 03bbc0c2f..4aec6cc35 100644 --- a/messages/en.json +++ b/messages/en.json @@ -308,6 +308,7 @@ "return-app.return-request-details.order-id.link": "Order {orderId}", "return-app.return-request-details.current-status.request-id": "Request ID: {id}", "return-app.return-request-details.current-status.status": "Status:", + "return-app.return-request-details.current-status.see-return-label": "See return label", "admin/return-app.return-request-list.table-data.requestId.tooltip": "Customers can see their request ID inside the request details", "admin/return-app.settings.modal-warning.title": "Please review your settings", "admin/return-app.settings.modal-warning.first-paragraph": "It looks like you are trying to save custom return options, none of which reach {maxDays}. This is the maximum days set for a return.", @@ -315,11 +316,18 @@ "admin/return-app.settings.modal-warning.third-paragraph": "If you click Edit, the maximum days for a return will be reduced from {maxDays} to {customMaxDays}. This is the Max Days shown in your custom return options.", "admin/return-app.settings.modal-warning.confirm": "Edit", "admin/return-app.settings.modal-warning.cancel": "Cancel", - "store/return-app.return-order-details.pickup-address.drop-off-points": "Select a drop-off point", - "store/return-app.return-order-details.pickup-address.drop-off-points.tooltip": "You can return your items in one of our selected drop-off points", - "store/return-app.return-order-details.pickup-address.drop-off-points.dropdown.placehoder": "Select a drop-off point", - "store/return-app.return-order-details.pickup-address.drop-off-points.dropdown.error": "There was an error loading the drop-off points", + "store/return-app.return-order-details.pickup-address.drop-off-points": "Select a drop off point", + "store/return-app.return-order-details.pickup-address.drop-off-points.tooltip": "You can return your items in one of our selected drop off points", + "store/return-app.return-order-details.pickup-address.drop-off-points.dropdown.placehoder": "Select a drop off point", + "store/return-app.return-order-details.pickup-address.drop-off-points.dropdown.error": "There was an error loading the drop off points", "admin/return-app.return-request-details.localized-product-name.tooltip": "Original item name from the order", + "admin/return-app.return-request-details.return-label.modal-title": "Are you sure you want to create a return shipping label?", + "admin/return-app.return-request-details.return-label.modal-message": "This action is irreversible. The created label will be sent via email to the customer.", + "admin/return-app.return-request-details.return-label.create-return-label": "Create return label", + "admin/return-app.return-request-details.return-label.see-return-label": "See return label", + "admin/return-app.return-request-details.return-label-info.tooltip": "Create a return shipping label and send it to the customer via email. The request status has to be processing. For this integration to work properly you must install and configure vtex.easypost.", + "admin/return-app.return-request-details.return-label.alert.success": "The return label was created successfully", + "admin/return-app.return-request-details.return-label.alert.error": "There was an error creating the return label, please try again.", "admin/return-app.settings.section.payment-options.refund-method-strategy.checkbox.label": "Automatically refund requests", "admin/return-app.settings.section.payment-options.refund-method-strategy.checkbox.description": "Automatically create a return invoice in OMS for the requests", "return-app.return-request-details.payent-method.refund-option.refund-process": "Refund process: {automaticallyRefundPaymentMethod, select, true{Automatic} false{Manual}}", diff --git a/node/clients/mail.ts b/node/clients/mail.ts index 8aee9ad4c..aeaeca055 100644 --- a/node/clients/mail.ts +++ b/node/clients/mail.ts @@ -5,6 +5,7 @@ import type { StatusUpdateMailData, ConfirmationMailData, Template, + ReturnLabelMailData, } from '../typings/mailClient' const MAIL_SERVICE_PATH = '/api/mail-service/pvt/sendmail' @@ -21,7 +22,7 @@ export class MailClient extends JanusClient { } public sendMail( - mailData: StatusUpdateMailData | ConfirmationMailData + mailData: StatusUpdateMailData | ConfirmationMailData | ReturnLabelMailData ): Promise { return this.http.post(MAIL_SERVICE_PATH, mailData, { metric: 'mail-post-send', @@ -34,7 +35,7 @@ export class MailClient extends JanusClient { }) } - public publishTemplate(template: Template): Promise { + public publishTemplate(template: Template): Promise { return this.http.post(TEMPLATE_RENDER_PATH, template, { headers: { ...this.options?.headers, diff --git a/node/package.json b/node/package.json index a366b2464..706da46b1 100644 --- a/node/package.json +++ b/node/package.json @@ -16,6 +16,7 @@ "@vtex/tsconfig": "^0.6.0", "tslint": "^6.1.3", "tslint-config-vtex": "^2.1.0", + "vtex.apps-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.apps-graphql@3.12.1/public/@types/vtex.apps-graphql", "vtex.catalog-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.101.1/public/@types/vtex.catalog-graphql", "vtex.css-handles": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles", "vtex.easypost": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.easypost@0.1.1/public/@types/vtex.easypost", @@ -23,9 +24,9 @@ "vtex.my-account": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.my-account@1.25.0/public/@types/vtex.my-account", "vtex.my-account-commons": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.my-account-commons@1.6.0/public/@types/vtex.my-account-commons", "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.132.4/public/@types/vtex.render-runtime", - "vtex.return-app": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.return-app@3.5.0/public/@types/vtex.return-app", + "vtex.return-app": "https://rraep--powerplanet.myvtex.com/_v/private/typings/linked/v1/vtex.return-app@3.5.0+build1662483831/public/@types/vtex.return-app", "vtex.store-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.155.32/public/@types/vtex.store-graphql", - "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.1/public/@types/vtex.styleguide", + "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.2/public/@types/vtex.styleguide", "vtex.tenant-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql" }, "scripts": { diff --git a/node/resolvers/index.ts b/node/resolvers/index.ts index e462ffcbf..30159af0d 100644 --- a/node/resolvers/index.ts +++ b/node/resolvers/index.ts @@ -11,10 +11,12 @@ import { returnRequestList } from './returnRequestList' import { ReturnRequestResponse } from './ReturnRequestResponse' import { updateReturnRequestStatus } from './updateReturnRequestStatus' import { nearestPickupPoints } from './nearestPickupPoints' +import { sendReturnLabel } from './sendReturnLabel' export const mutations = { createReturnRequest, updateReturnRequestStatus, + sendReturnLabel, ...settingsMutation, } diff --git a/node/resolvers/sendReturnLabel.ts b/node/resolvers/sendReturnLabel.ts new file mode 100644 index 000000000..e9bd88204 --- /dev/null +++ b/node/resolvers/sendReturnLabel.ts @@ -0,0 +1,16 @@ +import { sendReturnLabelService } from '../services/sendReturnLabelService' + +interface QuerySendReturnLabelArgs { + requestId: string + labelUrl: string +} + +export const sendReturnLabel = async ( + _: unknown, + args: QuerySendReturnLabelArgs, + ctx: Context +) => { + const { requestId, labelUrl } = args + + return sendReturnLabelService(ctx, requestId, labelUrl) +} diff --git a/node/services/createReturnRequestService.ts b/node/services/createReturnRequestService.ts index 55c30b8ab..fa9707063 100644 --- a/node/services/createReturnRequestService.ts +++ b/node/services/createReturnRequestService.ts @@ -2,10 +2,7 @@ import type { ReturnRequestCreated, ReturnRequestInput } from 'vtex.return-app' import { UserInputError, ResolverError } from '@vtex/api' import type { DocumentResponse } from '@vtex/clients' -import { - SETTINGS_PATH, - OMS_RETURN_REQUEST_CONFIRMATION, -} from '../utils/constants' +import { SETTINGS_PATH } from '../utils/constants' import { isUserAllowed } from '../utils/isUserAllowed' import { canOrderBeReturned } from '../utils/canOrderBeReturned' import { canReturnAllItems } from '../utils/canReturnAllItems' @@ -17,6 +14,7 @@ import { createRefundableTotals } from '../utils/createRefundableTotals' import { OMS_RETURN_REQUEST_CONFIRMATION_TEMPLATE } from '../utils/templates' import type { ConfirmationMailData } from '../typings/mailClient' import { getCustomerEmail } from '../utils/getCostumerEmail' +import { templateName } from '../utils/emailTemplates' import { validateItemCondition } from '../utils/validateItemCondition' export const createReturnRequestService = async ( @@ -285,7 +283,7 @@ export const createReturnRequestService = async ( // We add a try/catch here so we avoid sending an error to the browser only if the email fails. try { const templateExists = await mail.getTemplate( - OMS_RETURN_REQUEST_CONFIRMATION(locale) + templateName('confirmation', locale) ) if (!templateExists) { @@ -305,7 +303,7 @@ export const createReturnRequestService = async ( } = shippingData const mailData: ConfirmationMailData = { - templateName: OMS_RETURN_REQUEST_CONFIRMATION(locale), + templateName: templateName('confirmation', locale), jsonData: { data: { status: 'new', diff --git a/node/services/sendReturnLabelService.ts b/node/services/sendReturnLabelService.ts new file mode 100644 index 000000000..e9738e03c --- /dev/null +++ b/node/services/sendReturnLabelService.ts @@ -0,0 +1,96 @@ +import { ResolverError } from '@vtex/api' +import type { ReturnRequest } from 'vtex.return-app' + +import type { ReturnLabelMailData } from '../typings/mailClient' +import { templateName } from '../utils/emailTemplates' +import { OMS_RETURN_REQUEST_LABEL_TEMPLATE } from '../utils/templates' +import { formatRequestToPartialUpdate } from './updateRequestStatusService' + +export const sendReturnLabelService = async ( + ctx: Context, + requestId: string, + labelUrl: string +) => { + const { + clients: { mail, returnRequest: returnRequestClient }, + vtex: { logger }, + } = ctx + + if (!requestId || !labelUrl) { + throw new ResolverError( + 'The requestId or the labelUrl was not provided', + 400 + ) + } + + const returnRequest = (await returnRequestClient.get(requestId, [ + '_all', + ])) as ReturnRequest + + const { + pickupReturnData, + cultureInfoData: { locale }, + } = returnRequest + + const updatedPickupReturnData = { + ...pickupReturnData, + labelUrl, + } + + const updatedRequest = { + ...formatRequestToPartialUpdate(returnRequest), + pickupReturnData: updatedPickupReturnData, + } + + try { + await returnRequestClient.update(requestId, updatedRequest) + } catch (error) { + const mdValidationErrors = error?.response?.data?.errors[0]?.errors + + const errorMessageString = mdValidationErrors + ? JSON.stringify( + { + message: 'Schema Validation error', + errors: mdValidationErrors, + }, + null, + 2 + ) + : error.message + + throw new ResolverError(errorMessageString, error.response?.status || 500) + } + + try { + const templateExists = await mail.getTemplate(templateName('label', locale)) + + if (!templateExists) { + await mail.publishTemplate(OMS_RETURN_REQUEST_LABEL_TEMPLATE(locale)) + } + + const { customerProfileData } = updatedRequest + + const mailData: ReturnLabelMailData = { + templateName: templateName('label', locale), + jsonData: { + data: { + name: customerProfileData.name, + DocumentId: requestId, + email: customerProfileData.email, + labelUrl, + }, + }, + } + + await mail.sendMail(mailData) + } catch (error) { + logger.warn({ + message: `Failed to send email for return request ${requestId}`, + error, + }) + + return false + } + + return true +} diff --git a/node/services/updateRequestStatusService.ts b/node/services/updateRequestStatusService.ts index f1d21ead3..e9c7d6392 100644 --- a/node/services/updateRequestStatusService.ts +++ b/node/services/updateRequestStatusService.ts @@ -15,14 +15,14 @@ import { validateStatusUpdate } from '../utils/validateStatusUpdate' import { createOrUpdateStatusPayload } from '../utils/createOrUpdateStatusPayload' import { createRefundData } from '../utils/createRefundData' import { handleRefund } from '../utils/handleRefund' -import { OMS_RETURN_REQUEST_STATUS_UPDATE } from '../utils/constants' import { OMS_RETURN_REQUEST_STATUS_UPDATE_TEMPLATE } from '../utils/templates' import type { StatusUpdateMailData } from '../typings/mailClient' +import { templateName } from '../utils/emailTemplates' // A partial update on MD requires all required field to be sent. https://vtex.slack.com/archives/C8EE14F1C/p1644422359807929 // And the request to update fails when we pass the auto generated ones. // If any new field is added to the ReturnRequest as required, it has to be added here too. -const formatRequestToPartialUpdate = ( +export const formatRequestToPartialUpdate = ( request: ReturnRequest ): ReturnRequest => { const { @@ -232,7 +232,7 @@ export const updateRequestStatusService = async ( // We add a try/catch here so we avoid sending an error to the browser only if the email fails. try { const templateExists = await mail.getTemplate( - OMS_RETURN_REQUEST_STATUS_UPDATE(cultureInfoData?.locale) + templateName('status-update', cultureInfoData.locale) ) if (!templateExists) { @@ -251,7 +251,7 @@ export const updateRequestStatusService = async ( } = updatedRequest const mailData: StatusUpdateMailData = { - templateName: OMS_RETURN_REQUEST_STATUS_UPDATE(cultureInfoData?.locale), + templateName: templateName('status-update', cultureInfoData.locale), jsonData: { data: { status: updatedStatus, @@ -260,15 +260,22 @@ export const updateRequestStatusService = async ( email: customerProfileData?.email ?? '', paymentMethod: refundPaymentData?.refundPaymentMethod ?? '', iban: refundPaymentData?.iban ?? '', - refundedAmount: - Number(updatedRefundData?.refundedItemsValue) + - Number(updatedRefundData?.refundedShippingValue), }, products: items, refundStatusData: updatedRefundStatusData, }, } + if (updatedRefundData?.refundedItemsValue) { + let refundedAmount = Number(updatedRefundData.refundedItemsValue) + + if (updatedRefundData.refundedShippingValue) { + refundedAmount += Number(updatedRefundData.refundedShippingValue) + } + + mailData.jsonData.data.refundedAmount = refundedAmount + } + await mail.sendMail(mailData) } catch (error) { logger.warn({ diff --git a/node/typings/mailClient.d.ts b/node/typings/mailClient.d.ts index ddf9d8987..a14342de3 100644 --- a/node/typings/mailClient.d.ts +++ b/node/typings/mailClient.d.ts @@ -1,45 +1,64 @@ -import type { ReturnRequestItem, Status } from 'vtex.return-app' +import type { + ReturnRequest, + RefundStatusData, + ReturnRequestItem, + Status, +} from 'vtex.return-app' -export type ReturnRequestConfirmation = string +interface MailData { + templateName: string + jsonData: DataType +} -export type ReturnRequestStatusUpdate = string +interface ConfirmationData { + data: { + status: Status['new'] + name: string + DocumentId: string + email: string + phoneNumber: string + country: string + locality: string + address: string + paymentMethod: string + } + products: ReturnRequestItem[] + refundStatusData: RefundStatusData[] +} -export interface ConfirmationMailData { - templateName: ReturnRequestConfirmation - jsonData: { - data: { - status: Status['new'] - name: string - DocumentId: string - email: string - phoneNumber: string - country: string - locality: string - address: string - paymentMethod: string - } - products: ReturnRequestItem[] - refundStatusData: ReturnRequest['refundStatusData'] +interface StatusUpdateData { + data: { + status: Status + name: string + DocumentId: string + email: string + paymentMethod: string + iban: string + refundedAmount?: number } + products: ReturnRequest['items'] + refundStatusData: RefundStatusData[] } -export interface StatusUpdateMailData { - templateName: ReturnRequestStatusUpdate - jsonData: { - data: { - status: Status - name: string - DocumentId: string - email: string - paymentMethod: string - iban: string - refundedAmount: number - } - products: ReturnRequest['items'] - refundStatusData: ReturnRequest['refundStatusData'] +interface ReturnLabelData { + data: { + name: string + DocumentId: string + email: string + labelUrl: string } } +export type TemplateName = 'confirmation' | 'status-update' | 'label' + +export type TemplateFriendlyName = 'Confirmation' | 'Status Update' | 'Label' + +export type ConfirmationMailData = MailData + +export type StatusUpdateMailData = MailData + +export type ReturnLabelMailData = MailData + export interface Template { AccountId: string | null AccountName: string | null diff --git a/node/utils/constants.ts b/node/utils/constants.ts index 6ddfc5ddd..8ac906035 100644 --- a/node/utils/constants.ts +++ b/node/utils/constants.ts @@ -1,10 +1,5 @@ import type { OrderToReturnValidation } from 'vtex.return-app' -import type { - ReturnRequestConfirmation, - ReturnRequestStatusUpdate, -} from '../typings/mailClient' - export const SETTINGS_PATH = 'app-settings' export const ORDER_TO_RETURN_VALIDATON: Record< @@ -14,17 +9,3 @@ export const ORDER_TO_RETURN_VALIDATON: Record< OUT_OF_MAX_DAYS: 'OUT_OF_MAX_DAYS', ORDER_NOT_INVOICED: 'ORDER_NOT_INVOICED', } - -export const OMS_RETURN_REQUEST_CONFIRMATION = ( - locale = 'en-GB' -): ReturnRequestConfirmation => `oms-return-request-confirmation_${locale}` -export const OMS_RETURN_REQUEST_CONFIRMATION_FRIENDLY_NAME = ( - locale = 'en-GB' -) => `[OMS] Return Request Confirmation_${locale}` - -export const OMS_RETURN_REQUEST_STATUS_UPDATE = ( - locale = 'en-GB' -): ReturnRequestStatusUpdate => `oms-return-request-status-update_${locale}` -export const OMS_RETURN_REQUEST_STATUS_UPDATE_FRIENDLY_NAME = ( - locale = 'en-GB' -) => `[OMS] Return Request Status Update_${locale}` diff --git a/node/utils/emailTemplates.ts b/node/utils/emailTemplates.ts new file mode 100644 index 000000000..edce00dba --- /dev/null +++ b/node/utils/emailTemplates.ts @@ -0,0 +1,21 @@ +import type { TemplateFriendlyName, TemplateName } from '../typings/mailClient' + +/** + * + * @param name Template name (omit the prefix - oms-return-request-) + * @param locale Locale (defaults to 'en-GB') + * @returns {String} Template name + */ +export const templateName = (name: TemplateName, locale: string): string => + `oms-return-request-${name}_${locale}` + +/** + * + * @param { String } name Template friendly name (omit the prefix - [OMS] Return Request) + * @param { String } locale Locale (defaults to 'en-GB') + * @returns { String } Template friendly name + */ +export const templateFriendlyName = ( + name: TemplateFriendlyName, + locale: string +): string => `[OMS] Return Request ${name} (${locale})` diff --git a/node/utils/templates/index.ts b/node/utils/templates/index.ts index eebd291ef..5a86089ef 100644 --- a/node/utils/templates/index.ts +++ b/node/utils/templates/index.ts @@ -1,20 +1,16 @@ import type { Template } from '../../typings/mailClient' -import { - OMS_RETURN_REQUEST_CONFIRMATION, - OMS_RETURN_REQUEST_CONFIRMATION_FRIENDLY_NAME, - OMS_RETURN_REQUEST_STATUS_UPDATE, - OMS_RETURN_REQUEST_STATUS_UPDATE_FRIENDLY_NAME, -} from '../constants' +import { templateFriendlyName, templateName } from '../emailTemplates' import { OMS_RETURN_REQUEST_CONFIRMATION_TEMPLATE_MESSAGE, + OMS_RETURN_REQUEST_LABEL_TEMPLATE_MESSAGE, OMS_RETURN_REQUEST_STATUS_UPDATE_TEMPLATE_MESSAGE, } from './templateMessages' export const OMS_RETURN_REQUEST_CONFIRMATION_TEMPLATE = ( locale = 'en-GB' ): Template => ({ - Name: OMS_RETURN_REQUEST_CONFIRMATION(locale), - FriendlyName: OMS_RETURN_REQUEST_CONFIRMATION_FRIENDLY_NAME(locale), + Name: templateName('confirmation', locale), + FriendlyName: templateFriendlyName('Confirmation', locale), Description: '[OMS] Return requests confirmation message', IsDefaultTemplate: false, AccountId: null, @@ -50,8 +46,8 @@ export const OMS_RETURN_REQUEST_CONFIRMATION_TEMPLATE = ( export const OMS_RETURN_REQUEST_STATUS_UPDATE_TEMPLATE = ( locale = 'en-GB' ): Template => ({ - Name: OMS_RETURN_REQUEST_STATUS_UPDATE(locale), - FriendlyName: OMS_RETURN_REQUEST_STATUS_UPDATE_FRIENDLY_NAME(locale), + Name: templateName('status-update', locale), + FriendlyName: templateFriendlyName('Status Update', locale), Description: '[OMS] Return requests status update message', IsDefaultTemplate: false, AccountId: null, @@ -83,3 +79,40 @@ export const OMS_RETURN_REQUEST_STATUS_UPDATE_TEMPLATE = ( }, }, }) + +export const OMS_RETURN_REQUEST_LABEL_TEMPLATE = ( + locale = 'en-GB' +): Template => ({ + Name: templateName('label', locale), + FriendlyName: templateFriendlyName('Label', locale), + Description: '[OMS] Return requests shipping label message', + IsDefaultTemplate: false, + AccountId: null, + AccountName: null, + ApplicationId: null, + IsPersisted: true, + IsRemoved: false, + Type: '', + Templates: { + email: { + To: '{{data.email}}', + CC: null, + BCC: '{{#compare data.status "==" \'new\'}}{{/compare}}', + Subject: 'Return request shipping label {{data.DocumentId}}', + Message: OMS_RETURN_REQUEST_LABEL_TEMPLATE_MESSAGE, + Type: 'E', + ProviderId: '00000000-0000-0000-0000-000000000000', + ProviderName: null, + IsActive: true, + withError: false, + }, + sms: { + Type: 'S', + ProviderId: null, + ProviderName: null, + IsActive: false, + withError: false, + Parameters: [], + }, + }, +}) diff --git a/node/utils/templates/templateMessages.ts b/node/utils/templates/templateMessages.ts index 727895650..657f349bd 100644 --- a/node/utils/templates/templateMessages.ts +++ b/node/utils/templates/templateMessages.ts @@ -409,6 +409,29 @@ const STATUS_TIMELINE = html` {{/compare}} ` +const RETURN_LABEL = html` + + +
+

+ Return shipping label +

+

+ You can find the return shipping label to return your items in the + next link: +

+ Return shipping label +
+ + +` + const createTemplate = (headAndStyle: string, ...bodyBlocks: string[]) => { return ` { <> +
diff --git a/react/admin/ReturnDetails/components/ReturnLabel/ReturnLabel.tsx b/react/admin/ReturnDetails/components/ReturnLabel/ReturnLabel.tsx new file mode 100644 index 000000000..4ea6cb2fa --- /dev/null +++ b/react/admin/ReturnDetails/components/ReturnLabel/ReturnLabel.tsx @@ -0,0 +1,193 @@ +import React, { Fragment, useState } from 'react' +import { useMutation, useQuery } from 'react-apollo' +import { FormattedMessage } from 'react-intl' +import { + ButtonPlain, + Collapsible, + Link, + ModalDialog, + Tooltip, + IconInfo, +} from 'vtex.styleguide' + +import { useReturnDetails } from '../../../../common/hooks/useReturnDetails' +import { useAlert } from '../../../hooks/userAlert' +import GET_APP from './graphql/getInstalledApp.gql' +import SEND_LABEL from './graphql/sendLabel.gql' +import CREATE_LABEL from './graphql/createLabel.gql' + +const ReturnLabel = () => { + const { data } = useReturnDetails() + const { openAlert } = useAlert() + + const [labelUrl, setLabelUrl] = useState( + data?.returnRequestDetails.pickupReturnData.labelUrl ?? '' + ) + + const [returnAddress, setReturnAddress] = useState( + null + ) + + const [isModalOpen, setIsModalOpen] = useState(false) + const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(false) + + const { loading, error } = useQuery<{ + app: { + settings: string + } + }>(GET_APP, { + variables: { + slug: 'vtex.easypost', + }, + onCompleted(installedApp) { + const { + app: { settings }, + } = installedApp + + const { street1, street2, city, state, zip, country, name, phone } = + JSON.parse(settings) as ReturnLabelAddress + + setReturnAddress({ + street1, + street2, + city, + state, + zip, + country, + name, + phone, + }) + }, + }) + + const handleToggleModal = () => { + setIsModalOpen(!isModalOpen) + } + + const handleToggleCollapsible = () => { + setIsCollapsibleOpen(!isCollapsibleOpen) + } + + const handleCancelation = () => { + setIsModalOpen(false) + } + + const [, { loading: loadingLabel }] = useMutation(CREATE_LABEL) + const [sendLabel, { loading: sendingEmail }] = useMutation(SEND_LABEL) + + const handleConfirmation = async () => { + // temp labelUrl [DELETE when we have the client Key from easypost] + const createdLabelUrl = + 'https://assets.easypost.com/assets/images/usps-international-label.c7c603e0b25b12e4489a8c75db0d34b8.png' + + try { + // Send mutation to EasyPost + // const createdLabelUrl = await createLabel({ + // variables: { + // ...returnAddress, + // }, + // }) + + await sendLabel({ + variables: { + requestId: data?.returnRequestDetails.id, + labelUrl: createdLabelUrl, + }, + }) + + setLabelUrl(createdLabelUrl) + + openAlert( + 'success', + + ) + } catch (err) { + openAlert( + 'error', + + ) + } + + setIsModalOpen(false) + } + + if (loading || error) return null + + return ( +
+
+ {returnAddress && + (labelUrl === '' ? ( + + + + +
+ + } + position="right" + > +
+ + + +
+
+
+
+ ) : ( + + +
+ } + isOpen={isCollapsibleOpen} + onClick={handleToggleCollapsible} + > +
+ + {labelUrl} + +
+ + ))} +
+ + + ), + onClick: handleConfirmation, + }} + cancelation={{ + label: ( + + ), + onClick: handleCancelation, + }} + > +
+

+ +

+

+ +

+
+
+
+ ) +} + +export { ReturnLabel } diff --git a/react/graphql/createLabel.gql b/react/admin/ReturnDetails/components/ReturnLabel/graphql/createLabel.gql similarity index 100% rename from react/graphql/createLabel.gql rename to react/admin/ReturnDetails/components/ReturnLabel/graphql/createLabel.gql diff --git a/react/admin/ReturnDetails/components/ReturnLabel/graphql/getInstalledApp.gql b/react/admin/ReturnDetails/components/ReturnLabel/graphql/getInstalledApp.gql new file mode 100644 index 000000000..c95ac99b5 --- /dev/null +++ b/react/admin/ReturnDetails/components/ReturnLabel/graphql/getInstalledApp.gql @@ -0,0 +1,5 @@ +query GetInstalleApp($slug: String!) { + app: installedAppPublic(slug: $slug) @context(provider: "vtex.apps-graphql") { + settings + } +} diff --git a/react/admin/ReturnDetails/components/ReturnLabel/graphql/sendLabel.gql b/react/admin/ReturnDetails/components/ReturnLabel/graphql/sendLabel.gql new file mode 100644 index 000000000..2a176d0d0 --- /dev/null +++ b/react/admin/ReturnDetails/components/ReturnLabel/graphql/sendLabel.gql @@ -0,0 +1,4 @@ +mutation SendLabel($requestId: String!, $labelUrl: String!) { + sendReturnLabel(requestId: $requestId, labelUrl: $labelUrl) + @context(provider: "vtex.return-app") +} diff --git a/react/common/components/ReturnDetails/ReturnLabel.tsx b/react/common/components/ReturnDetails/ReturnLabel.tsx new file mode 100644 index 000000000..8e9b84bfa --- /dev/null +++ b/react/common/components/ReturnDetails/ReturnLabel.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react' +import { FormattedMessage } from 'react-intl' +import { useCssHandles } from 'vtex.css-handles' +import { Collapsible, Link } from 'vtex.styleguide' + +import { useReturnDetails } from '../../hooks/useReturnDetails' + +const CSS_HANDLES = ['returnLabelContainer'] as const + +const ReturnLabel = () => { + const [isOpen, setIsOpen] = useState(false) + const handles = useCssHandles(CSS_HANDLES) + + const { data } = useReturnDetails() + + const handleToggleCollapsible = () => { + setIsOpen(!isOpen) + } + + if (!data) return null + + const { + pickupReturnData: { labelUrl }, + } = data.returnRequestDetails + + return ( +
+
+ + +
+ } + isOpen={isOpen} + onClick={handleToggleCollapsible} + > + + {labelUrl} + + +
+ + ) +} + +export { ReturnLabel } diff --git a/react/common/graphql/returnDetailsFragment.gql b/react/common/graphql/returnDetailsFragment.gql index 429b52db7..8712e3740 100644 --- a/react/common/graphql/returnDetailsFragment.gql +++ b/react/common/graphql/returnDetailsFragment.gql @@ -45,6 +45,7 @@ fragment ReturnDetailsAdminFragment on ReturnRequestResponse { state zipCode addressType + labelUrl } refundPaymentData { refundPaymentMethod diff --git a/react/package.json b/react/package.json index 615e4e268..11b28d970 100644 --- a/react/package.json +++ b/react/package.json @@ -26,6 +26,7 @@ "@vtex/test-tools": "^0.3.2", "@vtex/tsconfig": "^0.6.0", "prop-types": "^15.7.2", + "vtex.apps-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.apps-graphql@3.12.1/public/@types/vtex.apps-graphql", "vtex.catalog-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.101.1/public/@types/vtex.catalog-graphql", "vtex.css-handles": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles", "vtex.easypost": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.easypost@0.1.1/public/@types/vtex.easypost", @@ -33,9 +34,9 @@ "vtex.my-account": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.my-account@1.25.0/public/@types/vtex.my-account", "vtex.my-account-commons": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.my-account-commons@1.6.0/public/@types/vtex.my-account-commons", "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.132.4/public/@types/vtex.render-runtime", - "vtex.return-app": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.return-app@3.5.0/public/@types/vtex.return-app", + "vtex.return-app": "https://rraep--powerplanet.myvtex.com/_v/private/typings/linked/v1/vtex.return-app@3.5.0+build1662483831/public/@types/vtex.return-app", "vtex.store-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.155.32/public/@types/vtex.store-graphql", - "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.1/public/@types/vtex.styleguide", + "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.2/public/@types/vtex.styleguide", "vtex.tenant-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql" }, "version": "3.5.0" diff --git a/react/store/ReturnRequest/StoreReturnDetails.tsx b/react/store/ReturnRequest/StoreReturnDetails.tsx index ba29d9538..cd6d02509 100644 --- a/react/store/ReturnRequest/StoreReturnDetails.tsx +++ b/react/store/ReturnRequest/StoreReturnDetails.tsx @@ -20,6 +20,7 @@ import { CurrentRequestStatus } from '../../common/components/ReturnDetails/Curr import RequestCancellation from '../../common/components/ReturnDetails/RequestCancellation' import { UpdateRequestStatusProvider } from '../../admin/provider/UpdateRequestStatusProvider' import { AlertProvider } from '../../admin/provider/AlertProvider' +import { ReturnLabel } from '../../common/components/ReturnDetails/ReturnLabel' const CSS_HANDLES = ['contactPickupContainer'] as const @@ -53,6 +54,7 @@ const StoreReturnDetails = () => { +
= T | null type GeoCoordinates = Array> diff --git a/react/yarn.lock b/react/yarn.lock index 494192068..8b932bde7 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -5178,6 +5178,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +"vtex.apps-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.apps-graphql@3.12.1/public/@types/vtex.apps-graphql": + version "3.12.1" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.apps-graphql@3.12.1/public/@types/vtex.apps-graphql#5e6ab3c2fa3d154d11798d6243585056bd2f7711" + "vtex.catalog-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.101.1/public/@types/vtex.catalog-graphql": version "1.101.1" resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.101.1/public/@types/vtex.catalog-graphql#8f4d8981c04429e94b30188acb9c6f1a6b912197" @@ -5206,17 +5210,17 @@ verror@1.10.0: version "8.132.4" resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.132.4/public/@types/vtex.render-runtime#66bb41bd4d342e37c9d85172aad5f7eefebfb6dc" -"vtex.return-app@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.return-app@3.5.0/public/@types/vtex.return-app": +"vtex.return-app@https://rraep--powerplanet.myvtex.com/_v/private/typings/linked/v1/vtex.return-app@3.5.0+build1662483831/public/@types/vtex.return-app": version "3.5.0" - resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.return-app@3.5.0/public/@types/vtex.return-app#29e8ca8294b52af366586fcea7ae995870bc0271" + resolved "https://rraep--powerplanet.myvtex.com/_v/private/typings/linked/v1/vtex.return-app@3.5.0+build1662483831/public/@types/vtex.return-app#2a0c0e4df36fe5fcebd79598dd0df7a3ee62dee7" "vtex.store-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.155.32/public/@types/vtex.store-graphql": version "2.155.32" resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.155.32/public/@types/vtex.store-graphql#abafbe1d80f453bbebc70024a829e4226bf6cc20" -"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.1/public/@types/vtex.styleguide": - version "9.146.1" - resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.1/public/@types/vtex.styleguide#66db40ca1b78ad77ce4beedabecee320e1ef2ead" +"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.2/public/@types/vtex.styleguide": + version "9.146.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.2/public/@types/vtex.styleguide#b0f097f3fef06316a916d6782cf077349bb34a3b" "vtex.tenant-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql": version "0.1.2"