diff --git a/masterdata/returnRequest/schema.json b/masterdata/returnRequest/schema.json index 1b3a06b2b..6fd235050 100644 --- a/masterdata/returnRequest/schema.json +++ b/masterdata/returnRequest/schema.json @@ -309,7 +309,8 @@ "customerProfileData", "status", "sequenceNumber", - "dateSubmitted" + "dateSubmitted", + "items" ], "v-immediate-indexing": true } diff --git a/node/clients/index.ts b/node/clients/index.ts index 2a1a2d876..81f729ff0 100644 --- a/node/clients/index.ts +++ b/node/clients/index.ts @@ -9,6 +9,7 @@ import { MailClient } from './mail' import Checkout from './checkout' import { VtexId } from './vtexId' import { CatalogGQL } from './catalogGQL' +import { MarketplaceAppClient } from './marketplace' const ReturnAppSettings = vbaseFor('appSettings') const ReturnRequest = masterDataFor('returnRequest') @@ -53,4 +54,8 @@ export class Clients extends IOClients { public get sphinx() { return this.getOrSet('sphinx', Sphinx) } + + public get marketplace() { + return this.getOrSet('marketplace', MarketplaceAppClient) + } } diff --git a/node/clients/marketplace.ts b/node/clients/marketplace.ts new file mode 100644 index 000000000..f8d483be2 --- /dev/null +++ b/node/clients/marketplace.ts @@ -0,0 +1,54 @@ +import type { InstanceOptions, IOContext } from '@vtex/api' +import { AuthType, IOClient } from '@vtex/api' +import { + ReturnRequestList, + ReturnRequest, + MutationUpdateReturnRequestStatusArgs, +} from 'vtex.return-app' + +const useHttps = !process.env.VTEX_IO + +export class MarketplaceAppClient extends IOClient { + constructor(context: IOContext, options?: InstanceOptions) { + super(context, { + ...options, + authType: AuthType.bearer, + baseURL: `${ + useHttps ? 'https' : 'http' + }://app.io.vtex.com/vtex.return-app/v3`, + name: 'return-app', + headers: { + ...options?.headers, + ...(context.authToken + ? { + // Will a request coming form the API have adminUserAUthToken? Does it matter? + // context.adminUserAuthToken ?? context.authToken, + // When using adminUserAuthToken, Sphinx parse the user email, not the app calling it. We can use it, but then we need to find a different away to get the seller calling it. + // with authToken we get user as: vrn--vtexsphinx--aws-us-east-1--powerplanet--filarmamvp--link_vtex.return-app@3.5.0 + VtexIdclientAutCookie: context.authToken, + } + : null), + 'Content-Type': 'application/json', + }, + }) + } + + public getRMAList = async (marketplace: string): Promise => + this.http.get(`/${marketplace}/filarmamvp/_v/return-request`) + + public getRMADetails = async ( + requestId: string, + marketplace: string + ): Promise => + this.http.get(`/${marketplace}/filarmamvp/_v/return-request/${requestId}`) + + public updateRMA = ( + requestId: string, + marketplace: string, + data: MutationUpdateReturnRequestStatusArgs + ): Promise => + this.http.put( + `/${marketplace}/filarmamvp/_v/return-request/${requestId}`, + data + ) +} diff --git a/node/index.ts b/node/index.ts index a6181e080..736f44c81 100644 --- a/node/index.ts +++ b/node/index.ts @@ -54,6 +54,13 @@ export default new Service({ GET: [errorHandler, auth, getRequest], PUT: [errorHandler, auth, updateRequestStatus], }), + _returnRequests: method({ + GET: [errorHandler, auth, getRequestList], + }), + _returnRequest: method({ + GET: [errorHandler, auth, getRequest], + PUT: [errorHandler, auth, updateRequestStatus], + }), }, graphql: { resolvers: { diff --git a/node/middlewares/auth.ts b/node/middlewares/auth.ts index 9cb1a8115..f5b961381 100644 --- a/node/middlewares/auth.ts +++ b/node/middlewares/auth.ts @@ -18,6 +18,8 @@ export async function auth(ctx: Context, next: () => Promise) { if (authenticatedUser) { const isAdmin = await sphinx.isAdmin(authenticatedUser.user) + // user when coming from another account: vrn--vtexsphinx--aws-us-east-1--powerplanet--filarmamvp--link_vtex.return-app@3.5.0 + // maybe use it to get the seller name. const { user, userId } = authenticatedUser state.userProfile = { diff --git a/node/service.json b/node/service.json index 709aa46e5..f66a2bc6e 100644 --- a/node/service.json +++ b/node/service.json @@ -12,6 +12,32 @@ "returnRequest": { "path": "/_v/return-request/:requestId", "public": true + }, + "_returnRequests": { + "path": "/_v/return-request", + "public": false, + "policies": [ + { + "effect": "allow", + "actions": ["get"], + "principals": [ + "vrn:apps:*:*:*:app/vtex.return-app@*" + ] + } + ] + }, + "_returnRequest": { + "path": "/_v/return-request/:requestId", + "public": false, + "policies": [ + { + "effect": "allow", + "actions": ["get"], + "principals": [ + "vrn:apps:*:*:*:app/vtex.return-app@*" + ] + } + ] } } } diff --git a/node/services/returnRequestListService.ts b/node/services/returnRequestListService.ts index 955f8d612..89e33dad6 100644 --- a/node/services/returnRequestListService.ts +++ b/node/services/returnRequestListService.ts @@ -5,6 +5,8 @@ import type { } from 'vtex.return-app' import { ForbiddenError } from '@vtex/api' +const { VTEX_ACCOUNT } = process.env + const filterDate = (date: string): string => { const newDate = new Date(date) const day = newDate.getDate() @@ -61,9 +63,10 @@ export const returnRequestListService = async ( getAllFields = false ) => { const { - clients: { returnRequest: returnRequestClient }, + clients: { returnRequest: returnRequestClient, marketplace }, request: { header }, state: { userProfile, appkey }, + vtex: { logger }, } = ctx const { page, perPage, filter } = args @@ -73,6 +76,54 @@ export const returnRequestListService = async ( role, } = userProfile ?? {} + logger.info({ + service: 'get return request list', + account: VTEX_ACCOUNT, + vtexProduct: header['x-vtex-product'], + args, + state: { + userProfile, + isAdmin: Boolean(appkey), + }, + }) + + // vrn--vtexsphinx--aws-us-east-1--powerplanet--filarmamvp--link_vtex.return-app@3.5.0 + const isAppRequester = userEmailProfile?.includes('vtexsphinx') ?? false + + console.log({ userEmailProfile }) + + const [, , , sellerRequester] = + userEmailProfile && isAppRequester ? userEmailProfile.split('--') : [] + + // In the marketplace side, add its name into the + const MARKETPLACENAME = 'vtexspain' + + // avoid infinite loop on vtexspain + // call marketplace + const marketplaceRequests = + // adapt marketplace.getRMAList to accept and send filter params + VTEX_ACCOUNT === 'powerplanet' + ? await marketplace.getRMAList(MARKETPLACENAME) + : null + + if (marketplaceRequests) { + console.log({ marketplaceRequests }) + + logger.info({ + service: 'marketplace response', + account: VTEX_ACCOUNT, + marketplaceRequests, + }) + } + + const adjustedMktPlaceRequests = + // modify marketplace request ids so we can use it in the quer to get the details. + // Should we do that in the mkt place side or seller side? + marketplaceRequests?.list.map(({ id, ...request }: any) => ({ + id: `${id}::${MARKETPLACENAME}`, + ...request, + })) ?? null + const { userId: userIdArg, userEmail: userEmailArg } = filter ?? {} const userIsAdmin = Boolean(appkey) || role === 'admin' @@ -98,9 +149,11 @@ export const returnRequestListService = async ( throw new ForbiddenError('Missing params to filter by store user') } - const adjustedFilter = requireFilterByUser - ? { ...filter, userId, userEmail } - : filter + const adjustedFilter = + // require user info (store user calling) AND is not a sellerCalling (seller is not admin) + requireFilterByUser && !sellerRequester + ? { ...filter, userId, userEmail } + : filter const resultFields = getAllFields ? ['_all'] @@ -113,6 +166,14 @@ export const returnRequestListService = async ( 'dateSubmitted', ] + const whereFilter = buildWhereClause(adjustedFilter) + + const whereWithSeller = whereFilter?.length + ? `${whereFilter} AND items.sellerId=${sellerRequester}` + : `items.sellerId=${sellerRequester}` + + console.log({ whereWithSeller, sellerRequester }) + const rmaSearchResult = await returnRequestClient.searchRaw( { page, @@ -120,14 +181,18 @@ export const returnRequestListService = async ( }, resultFields, 'dateSubmitted DESC', - buildWhereClause(adjustedFilter) + sellerRequester ? whereWithSeller : whereFilter ) const { data, pagination } = rmaSearchResult const { page: currentPage, pageSize, total } = pagination + const finalList = adjustedMktPlaceRequests + ? [...adjustedMktPlaceRequests, ...data] + : data + return { - list: data, + list: finalList, paging: { total, perPage: pageSize, diff --git a/node/services/returnRequestService.ts b/node/services/returnRequestService.ts index d4a25a722..97339d33c 100644 --- a/node/services/returnRequestService.ts +++ b/node/services/returnRequestService.ts @@ -3,25 +3,48 @@ import type { ReturnRequest } from 'vtex.return-app' export const returnRequestService = async (ctx: Context, requestId: string) => { const { - clients: { returnRequest: returnRequestClient }, + clients: { returnRequest: returnRequestClient, marketplace }, state: { userProfile, appkey }, } = ctx - const { userId, role } = userProfile ?? {} + const { userId, role, email } = userProfile ?? {} + + const isAppRequester = email?.includes('vtexsphinx') ?? false + + const [, , , sellerRequester] = + email && isAppRequester ? email.split('--') : [] + + const [requestIdSeller, targetMarketplace] = requestId.split('::') + + console.log({ + sellerRequester, + requestId, + targetMarketplace, + requestIdSeller, + }) + const userIsAdmin = Boolean(appkey) || role === 'admin' - const returnRequestResult = await returnRequestClient.get(requestId, ['_all']) + const returnRequestResult = targetMarketplace + ? await marketplace.getRMADetails(requestIdSeller, targetMarketplace) + : await returnRequestClient.get(requestId, ['_all']) if (!returnRequestResult) { // Code error 'E_HTTP_404' to match the one when failing to find and order by OMS throw new ResolverError(`Request ${requestId} not found`, 404, 'E_HTTP_404') } - const { customerProfileData } = returnRequestResult as ReturnRequest + const { customerProfileData, items } = returnRequestResult as ReturnRequest const requestBelongsToUser = userId === customerProfileData?.userId - if (!requestBelongsToUser && !userIsAdmin) { + const requestbelongsToSeller = items.some( + (item) => item.sellerId === sellerRequester + ) + + console.log({ requestbelongsToSeller }) + + if (!requestBelongsToUser && !userIsAdmin && !requestbelongsToSeller) { throw new ForbiddenError('User cannot access this request') } diff --git a/node/services/updateRequestStatusService.ts b/node/services/updateRequestStatusService.ts index f1d21ead3..67aa91867 100644 --- a/node/services/updateRequestStatusService.ts +++ b/node/services/updateRequestStatusService.ts @@ -19,6 +19,8 @@ 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' +const { VTEX_ACCOUNT } = process.env + // 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. @@ -97,15 +99,24 @@ export const updateRequestStatusService = async ( oms, giftCard: giftCardClient, mail, + marketplace, }, vtex: { logger }, } = ctx const { status, requestId, comment, refundData } = args + const [requestIdSeller, targetMarketplace] = requestId.split('::') + const { role, firstName, lastName, email, userId } = userProfile ?? {} + const isAppRequester = email?.includes('vtexsphinx') ?? false + + const [, , , sellerRequester] = + email && isAppRequester ? email.split('--') : [] + const requestDate = new Date().toISOString() + // TODO: Check this. When the update come from Seller, who will be the submitter? const submittedByNameOrEmail = firstName || lastName ? `${firstName} ${lastName}` : email @@ -117,6 +128,11 @@ export const updateRequestStatusService = async ( ) } + if (VTEX_ACCOUNT === 'powerplanet' && targetMarketplace) { + // args has requestId with the postfix ::marketplace. In the middleware, it will be replaced by the one in the URL (without it) + return marketplace.updateRMA(requestIdSeller, targetMarketplace, args) + } + const returnRequest = (await returnRequestClient.get(requestId, [ '_all', ])) as ReturnRequest @@ -131,7 +147,11 @@ export const updateRequestStatusService = async ( returnRequest.customerProfileData.userId === userId && returnRequest.status === 'new' - if (!userIsAdmin && !belongsToStoreUser) { + const requestBelogsToSeller = returnRequest.items.some( + (item) => item.sellerId === sellerRequester + ) + + if (!userIsAdmin && !belongsToStoreUser && !requestBelogsToSeller) { throw new ForbiddenError('Not authorized') } @@ -183,6 +203,8 @@ export const updateRequestStatusService = async ( }) : returnRequest.refundData + // Client for GiftCard uses adminUserAuthToken. How to handle that on a request coming from seller? + // OMS client to create invoice also uses adminUserAuthToken. const refundReturn = await handleRefund({ currentStatus: requestStatus, previousStatus: returnRequest.status, @@ -236,6 +258,7 @@ export const updateRequestStatusService = async ( ) if (!templateExists) { + // mail.publishTemplate uses adminUserAuthToken. How to handle that on a request coming from seller? await mail.publishTemplate( OMS_RETURN_REQUEST_STATUS_UPDATE_TEMPLATE(cultureInfoData?.locale) ) diff --git a/react/admin/AdminReturnDetails.tsx b/react/admin/AdminReturnDetails.tsx index 4bad61502..25c33fc0b 100644 --- a/react/admin/AdminReturnDetails.tsx +++ b/react/admin/AdminReturnDetails.tsx @@ -15,7 +15,7 @@ export const AdminReturnDetails = ({ params }: CustomRouteProps) => { return ( - + diff --git a/react/admin/ReturnDetails/components/UpdateRequestStatus.tsx b/react/admin/ReturnDetails/components/UpdateRequestStatus.tsx index b45e652d7..d25cfc602 100644 --- a/react/admin/ReturnDetails/components/UpdateRequestStatus.tsx +++ b/react/admin/ReturnDetails/components/UpdateRequestStatus.tsx @@ -62,7 +62,6 @@ export const UpdateRequestStatus = ({ onViewVerifyItems }: Props) => { } handleStatusUpdate({ - id: data.returnRequestDetails?.id, status: selectedStatus, comment: newComment, cleanUp, diff --git a/react/admin/ReturnDetails/components/VerifyItems/VerifyItemsPage.tsx b/react/admin/ReturnDetails/components/VerifyItems/VerifyItemsPage.tsx index a51480bb7..3b3174c68 100644 --- a/react/admin/ReturnDetails/components/VerifyItems/VerifyItemsPage.tsx +++ b/react/admin/ReturnDetails/components/VerifyItems/VerifyItemsPage.tsx @@ -96,7 +96,6 @@ export const VerifyItemsPage = ({ onViewVerifyItems }: Props) => { handleStatusUpdate({ status: 'packageVerified', - id: requestId, refundData: { refundedShippingValue: shippingToRefund, items: itemsToRefund, diff --git a/react/admin/provider/UpdateRequestStatusProvider.tsx b/react/admin/provider/UpdateRequestStatusProvider.tsx index 262676725..fa8e74453 100644 --- a/react/admin/provider/UpdateRequestStatusProvider.tsx +++ b/react/admin/provider/UpdateRequestStatusProvider.tsx @@ -16,7 +16,6 @@ import { useReturnDetails } from '../../common/hooks/useReturnDetails' interface HandleStatusUpdateArgs { status: Status - id: string comment?: ReturnRequestCommentInput cleanUp?: () => void refundData?: RefundDataInput @@ -31,7 +30,14 @@ export const UpdateRequestStatusContext = createContext( {} as UpdateRequestInterface ) -export const UpdateRequestStatusProvider: FC = ({ children }) => { +interface CustomRouteProps { + requestId: string +} + +export const UpdateRequestStatusProvider: FC = ({ + children, + requestId, +}) => { const { openAlert } = useAlert() const { _handleUpdateQuery } = useReturnDetails() @@ -43,12 +49,12 @@ export const UpdateRequestStatusProvider: FC = ({ children }) => { >(UPDATE_RETURN_STATUS) const handleStatusUpdate = async (args: HandleStatusUpdateArgs) => { - const { id, status, comment, cleanUp, refundData } = args + const { status, comment, cleanUp, refundData } = args try { const { errors, data: mutationData } = await updateReturnStatus({ variables: { - requestId: id, + requestId, status, ...(comment ? { comment } : {}), ...(refundData ? { refundData } : {}), diff --git a/react/common/components/ReturnDetails/RequestCancellation.tsx b/react/common/components/ReturnDetails/RequestCancellation.tsx index 5eabe028b..21245170a 100644 --- a/react/common/components/ReturnDetails/RequestCancellation.tsx +++ b/react/common/components/ReturnDetails/RequestCancellation.tsx @@ -45,7 +45,7 @@ const RequestCancellation = () => { if (!data) return null - const { status, id } = data.returnRequestDetails + const { status } = data.returnRequestDetails const isDisabled = ['denied', 'cancelled'].includes(status) @@ -76,7 +76,6 @@ const RequestCancellation = () => { } handleStatusUpdate({ - id, status: 'cancelled', cleanUp: () => { onClose() diff --git a/react/store/ReturnRequest/StoreReturnDetails.tsx b/react/store/ReturnRequest/StoreReturnDetails.tsx index ba29d9538..28332e848 100644 --- a/react/store/ReturnRequest/StoreReturnDetails.tsx +++ b/react/store/ReturnRequest/StoreReturnDetails.tsx @@ -23,7 +23,7 @@ import { AlertProvider } from '../../admin/provider/AlertProvider' const CSS_HANDLES = ['contactPickupContainer'] as const -const StoreReturnDetails = () => { +const StoreReturnDetails = ({ requestId }: { requestId: string }) => { const { loading, error } = useReturnDetails() const { navigate } = useRuntime() const handles = useCssHandles(CSS_HANDLES) @@ -45,7 +45,7 @@ const StoreReturnDetails = () => { }} > - + @@ -75,7 +75,7 @@ export const StoreReturnDetailsContainer = ( ) => { return ( - + ) }