From edec671bb96bc1e2ef46eb1b86bdb6c1c3c0ed54 Mon Sep 17 00:00:00 2001 From: Filadelfo Date: Mon, 29 Aug 2022 10:48:48 -0300 Subject: [PATCH 1/3] MVP: Get and add list from marketplace on seller account --- masterdata/returnRequest/schema.json | 3 +- node/clients/index.ts | 5 ++ node/clients/marketplace.ts | 33 ++++++++++++ node/index.ts | 3 ++ node/middlewares/auth.ts | 2 + node/service.json | 13 +++++ node/services/returnRequestListService.ts | 64 ++++++++++++++++++++--- 7 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 node/clients/marketplace.ts 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..abc5823f3 --- /dev/null +++ b/node/clients/marketplace.ts @@ -0,0 +1,33 @@ +import type { InstanceOptions, IOContext } from '@vtex/api' +import { AuthType, IOClient } from '@vtex/api' + +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 (): Promise => + this.http.get('/vtexspain/filarmamvp/_v/return-request') +} diff --git a/node/index.ts b/node/index.ts index a6181e080..0dd646aa8 100644 --- a/node/index.ts +++ b/node/index.ts @@ -54,6 +54,9 @@ export default new Service({ GET: [errorHandler, auth, getRequest], PUT: [errorHandler, auth, updateRequestStatus], }), + _returnRequests: method({ + GET: [errorHandler, auth, getRequestList], + }), }, 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..ba55d1a78 100644 --- a/node/service.json +++ b/node/service.json @@ -12,6 +12,19 @@ "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@*" + ] + } + ] } } } diff --git a/node/services/returnRequestListService.ts b/node/services/returnRequestListService.ts index 955f8d612..efc32d97f 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,41 @@ 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('--') : [] + + // avoid infinite loop on vtexspain + // call marketplace + const marketplaceRequests = + // adapt marketplace.getRMAList to accept and send filter params + VTEX_ACCOUNT === 'powerplanet' ? await marketplace.getRMAList() : null + + if (marketplaceRequests) { + console.log({ marketplaceRequests }) + + logger.info({ + service: 'marketplace response', + account: VTEX_ACCOUNT, + marketplaceRequests, + }) + } + const { userId: userIdArg, userEmail: userEmailArg } = filter ?? {} const userIsAdmin = Boolean(appkey) || role === 'admin' @@ -98,9 +136,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 +153,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 +168,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 = marketplaceRequests + ? [...marketplaceRequests.list, ...data] + : data + return { - list: data, + list: finalList, paging: { total, perPage: pageSize, From 859bd7eb975c2384eb543ad1615e89f38f95385c Mon Sep 17 00:00:00 2001 From: Filadelfo Date: Mon, 29 Aug 2022 15:29:26 -0300 Subject: [PATCH 2/3] MVP: Get return request from marketplace on seller account --- node/clients/marketplace.ts | 11 ++++++-- node/index.ts | 3 +++ node/service.json | 13 +++++++++ node/services/returnRequestListService.ts | 19 ++++++++++--- node/services/returnRequestService.ts | 33 +++++++++++++++++++---- 5 files changed, 69 insertions(+), 10 deletions(-) diff --git a/node/clients/marketplace.ts b/node/clients/marketplace.ts index abc5823f3..cfab4eb30 100644 --- a/node/clients/marketplace.ts +++ b/node/clients/marketplace.ts @@ -1,5 +1,6 @@ import type { InstanceOptions, IOContext } from '@vtex/api' import { AuthType, IOClient } from '@vtex/api' +import { ReturnRequestList, ReturnRequest } from 'vtex.return-app' const useHttps = !process.env.VTEX_IO @@ -28,6 +29,12 @@ export class MarketplaceAppClient extends IOClient { }) } - public getRMAList = async (): Promise => - this.http.get('/vtexspain/filarmamvp/_v/return-request') + 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}`) } diff --git a/node/index.ts b/node/index.ts index 0dd646aa8..e975c7938 100644 --- a/node/index.ts +++ b/node/index.ts @@ -57,6 +57,9 @@ export default new Service({ _returnRequests: method({ GET: [errorHandler, auth, getRequestList], }), + _returnRequest: method({ + GET: [errorHandler, auth, getRequest], + }), }, graphql: { resolvers: { diff --git a/node/service.json b/node/service.json index ba55d1a78..f66a2bc6e 100644 --- a/node/service.json +++ b/node/service.json @@ -25,6 +25,19 @@ ] } ] + }, + "_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 efc32d97f..89e33dad6 100644 --- a/node/services/returnRequestListService.ts +++ b/node/services/returnRequestListService.ts @@ -95,11 +95,16 @@ export const returnRequestListService = async ( 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() : null + VTEX_ACCOUNT === 'powerplanet' + ? await marketplace.getRMAList(MARKETPLACENAME) + : null if (marketplaceRequests) { console.log({ marketplaceRequests }) @@ -111,6 +116,14 @@ export const returnRequestListService = async ( }) } + 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' @@ -174,8 +187,8 @@ export const returnRequestListService = async ( const { data, pagination } = rmaSearchResult const { page: currentPage, pageSize, total } = pagination - const finalList = marketplaceRequests - ? [...marketplaceRequests.list, ...data] + const finalList = adjustedMktPlaceRequests + ? [...adjustedMktPlaceRequests, ...data] : data return { 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') } From ff4ed2d8fb2d32bfa7f07461c7ef343557a089ed Mon Sep 17 00:00:00 2001 From: Filadelfo Date: Tue, 30 Aug 2022 16:51:09 -0300 Subject: [PATCH 3/3] MVP: Update request from seller side to marketplace side --- node/clients/marketplace.ts | 16 +++++++++++- node/index.ts | 1 + node/services/updateRequestStatusService.ts | 25 ++++++++++++++++++- react/admin/AdminReturnDetails.tsx | 2 +- .../components/UpdateRequestStatus.tsx | 1 - .../VerifyItems/VerifyItemsPage.tsx | 1 - .../provider/UpdateRequestStatusProvider.tsx | 14 ++++++++--- .../ReturnDetails/RequestCancellation.tsx | 3 +-- .../ReturnRequest/StoreReturnDetails.tsx | 6 ++--- 9 files changed, 55 insertions(+), 14 deletions(-) diff --git a/node/clients/marketplace.ts b/node/clients/marketplace.ts index cfab4eb30..f8d483be2 100644 --- a/node/clients/marketplace.ts +++ b/node/clients/marketplace.ts @@ -1,6 +1,10 @@ import type { InstanceOptions, IOContext } from '@vtex/api' import { AuthType, IOClient } from '@vtex/api' -import { ReturnRequestList, ReturnRequest } from 'vtex.return-app' +import { + ReturnRequestList, + ReturnRequest, + MutationUpdateReturnRequestStatusArgs, +} from 'vtex.return-app' const useHttps = !process.env.VTEX_IO @@ -37,4 +41,14 @@ export class MarketplaceAppClient extends IOClient { 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 e975c7938..736f44c81 100644 --- a/node/index.ts +++ b/node/index.ts @@ -59,6 +59,7 @@ export default new Service({ }), _returnRequest: method({ GET: [errorHandler, auth, getRequest], + PUT: [errorHandler, auth, updateRequestStatus], }), }, graphql: { 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 ( - + ) }