diff --git a/commerce/types.ts b/commerce/types.ts index 81967c7be..97667791d 100644 --- a/commerce/types.ts +++ b/commerce/types.ts @@ -351,6 +351,8 @@ export interface Review extends Omit { id?: string; /** Author of the */ author?: Author[]; + /** The date that the order was created, in ISO 8601 date format.*/ + dateCreated?: string; /** The date that the review was published, in ISO 8601 date format.*/ datePublished?: string; /** The item that is being reviewed/rated. */ diff --git a/verified-reviews/loaders/productDetailsPage.ts b/verified-reviews/loaders/productDetailsPage.ts index 917afa83a..dd7278444 100644 --- a/verified-reviews/loaders/productDetailsPage.ts +++ b/verified-reviews/loaders/productDetailsPage.ts @@ -6,7 +6,10 @@ import { getProductId, PaginationOptions, } from "../utils/client.ts"; -export type Props = PaginationOptions; + +export type Props = PaginationOptions & { + aggregateSimilarProducts?: boolean; +}; /** * @title Opiniões verificadas - Full Review for Product (Ratings and Reviews) @@ -17,18 +20,24 @@ export default function productDetailsPage( ctx: AppContext, ): ExtensionOf { const client = createClient({ ...ctx }); + return async (productDetailsPage: ProductDetailsPage | null) => { - if (!productDetailsPage) { + if (!productDetailsPage || !client) { return null; } - if (!client) { - return null; + const productId = getProductId(productDetailsPage.product); + let productsToGetReviews = [productId]; + + if (config.aggregateSimilarProducts) { + productsToGetReviews = [ + productId, + ...productDetailsPage.product.isSimilarTo?.map(getProductId) ?? [], + ]; } - const productId = getProductId(productDetailsPage.product); const fullReview = await client.fullReview({ - productId, + productId: productsToGetReviews, count: config?.count, offset: config?.offset, order: config?.order, diff --git a/verified-reviews/loaders/productReviews.ts b/verified-reviews/loaders/productReviews.ts new file mode 100644 index 000000000..af332f9de --- /dev/null +++ b/verified-reviews/loaders/productReviews.ts @@ -0,0 +1,33 @@ +import { AppContext } from "../mod.ts"; +import { Review } from "../../commerce/types.ts"; +import { createClient, PaginationOptions } from "../utils/client.ts"; +import { toReview } from "../utils/transform.ts"; + +export type Props = PaginationOptions & { + productId: string | string[]; +}; + +/** + * @title Opiniões verificadas - Full Review for Product (Ratings and Reviews) + */ +export default async function productReviews( + config: Props, + _req: Request, + ctx: AppContext, +): Promise { + const client = createClient({ ...ctx }); + + if (!client) { + return null; + } + + const reviewsResponse = await client.reviews({ + productId: config.productId, + count: config?.count, + offset: config?.offset, + order: config?.order, + }); + + const reviews = reviewsResponse?.[0]; + return reviews?.reviews?.map(toReview) ?? []; +} diff --git a/verified-reviews/manifest.gen.ts b/verified-reviews/manifest.gen.ts index b6f337c9e..8286cc66d 100644 --- a/verified-reviews/manifest.gen.ts +++ b/verified-reviews/manifest.gen.ts @@ -5,14 +5,16 @@ import * as $$$0 from "./loaders/productDetailsPage.ts"; import * as $$$1 from "./loaders/productList.ts"; import * as $$$2 from "./loaders/productListingPage.ts"; -import * as $$$3 from "./loaders/storeReview.ts"; +import * as $$$3 from "./loaders/productReviews.ts"; +import * as $$$4 from "./loaders/storeReview.ts"; const manifest = { "loaders": { "verified-reviews/loaders/productDetailsPage.ts": $$$0, "verified-reviews/loaders/productList.ts": $$$1, "verified-reviews/loaders/productListingPage.ts": $$$2, - "verified-reviews/loaders/storeReview.ts": $$$3, + "verified-reviews/loaders/productReviews.ts": $$$3, + "verified-reviews/loaders/storeReview.ts": $$$4, }, "name": "verified-reviews", "baseUrl": import.meta.url, diff --git a/verified-reviews/utils/client.ts b/verified-reviews/utils/client.ts index 4da18103a..55293bd1c 100644 --- a/verified-reviews/utils/client.ts +++ b/verified-reviews/utils/client.ts @@ -2,6 +2,11 @@ import { fetchAPI } from "../../utils/fetch.ts"; import { Ratings, Reviews, VerifiedReviewsFullReview } from "./types.ts"; import { Product } from "../../commerce/types.ts"; import { ConfigVerifiedReviews } from "../mod.ts"; +import { + getRatingProduct, + getWeightedRatingProduct, + toReview, +} from "./transform.ts"; import { context } from "@deco/deco"; export type ClientVerifiedReviews = ReturnType; export interface PaginationOptions { @@ -14,6 +19,16 @@ export interface PaginationOptions { | "rate_ASC" | "helpfulrating_DESC"; } + +// creating an object to keep backward compatibility +const orderMap = { + date_desc: "date_desc", + date_ASC: "date_asc", + rate_DESC: "rate_desc", + rate_ASC: "rate_asc", + helpfulrating_DESC: "most_helpful", +} as const; + const MessageError = { ratings: "🔴⭐ Error on call ratings of Verified Review - probably unidentified product", @@ -81,68 +96,64 @@ export const createClient = (params: ConfigVerifiedReviews | undefined) => { }; /** @description https://documenter.getpostman.com/view/2336519/SVzw6MK5#daf51360-c79e-451a-b627-33bdd0ef66b8 */ const reviews = ( - { productId, count = 5, offset = 0, order = "date_desc" }: + { productId, count = 5, offset = 0, order: _order = "date_desc" }: & PaginationOptions & { - productId: string; + productId: string | string[]; }, ) => { + const order = orderMap[_order]; + const payload = { query: "reviews", - product: productId, + product: Array.isArray(productId) ? productId : [productId], idWebsite: idWebsite, plateforme: "br", offset: offset, limit: count, order: order, }; - return fetchAPI(`${baseUrl}`, { + + return fetchAPI(`${baseUrl}`, { method: "POST", body: JSON.stringify(payload), }); }; - const fullReview = async ( - { productId, count = 5, offset = 0 }: PaginationOptions & { - productId: string; - }, - ): Promise => { + + const fullReview = async ({ + productId, + count = 5, + offset = 0, + order, + }: PaginationOptions & { + productId: string | string[]; + }): Promise => { try { + const isMultiProduct = Array.isArray(productId); + const response = await Promise.all([ - rating({ productId }), - reviews({ productId, count, offset }), + ratings({ + productsIds: isMultiProduct ? productId : [productId], + }), + reviews({ productId, count, offset, order }), ]); const [responseRating, responseReview] = response.flat() as [ Ratings, Reviews | null, ]; - const currentRating = responseRating?.[productId]?.[0]; + + const aggregateRating = isMultiProduct + ? getWeightedRatingProduct(responseRating) + : getRatingProduct({ ratings: responseRating, productId }); + return { - aggregateRating: currentRating + aggregateRating: aggregateRating ? { - "@type": "AggregateRating", - ratingValue: Number(parseFloat(currentRating.rate).toFixed(1)), - reviewCount: Number(currentRating.count), + ...aggregateRating, + stats: responseReview?.stats, } : undefined, - review: responseReview - ? responseReview.reviews?.map((item) => ({ - "@type": "Review", - author: [ - { - "@type": "Author", - name: `${item.firstname} ${item.lastname}`, - }, - ], - datePublished: item.review_date, - reviewBody: item.review, - reviewRating: { - "@type": "AggregateRating", - ratingValue: Number(item.rate), - // this api does not support multiple reviews - reviewCount: 1, - }, - })) - : [], + review: responseReview ? responseReview.reviews?.map(toReview) : [], }; } catch (error) { if (context.isDeploy) { diff --git a/verified-reviews/utils/transform.ts b/verified-reviews/utils/transform.ts index 8b8c8fee8..5d34ecab9 100644 --- a/verified-reviews/utils/transform.ts +++ b/verified-reviews/utils/transform.ts @@ -4,6 +4,9 @@ import { } from "../../commerce/types.ts"; import { Ratings, Review } from "./types.ts"; +const MAX_RATING_VALUE = 5; +const MIN_RATING_VALUE = 0; + export const getRatingProduct = ({ ratings, productId, @@ -12,15 +15,54 @@ export const getRatingProduct = ({ productId: string; }): AggregateRating | undefined => { const rating = ratings?.[productId]?.[0]; + if (!rating) { return undefined; } - return { + const aggregateRating: AggregateRating = { "@type": "AggregateRating", ratingCount: Number(rating.count), ratingValue: Number(parseFloat(rating.rate).toFixed(1)), + bestRating: MAX_RATING_VALUE, + worstRating: MIN_RATING_VALUE, }; + + return aggregateRating; +}; + +export const getWeightedRatingProduct = ( + ratings: Ratings | undefined, +): AggregateRating | undefined => { + if (!ratings) { + return undefined; + } + + const { weightedRating, totalRatings } = Object.entries(ratings ?? {}).reduce( + (acc, [_, [ratingDetails]]) => { + const count = Number(ratingDetails.count); + const value = Number(parseFloat(ratingDetails.rate).toFixed(1)); + + acc.totalRatings += count; + acc.weightedRating += count * value; + + return acc; + }, + { weightedRating: 0, totalRatings: 0 }, + ); + + const aggregateRating: AggregateRating = { + "@type": "AggregateRating", + ratingCount: totalRatings, + reviewCount: totalRatings, + ratingValue: totalRatings > 0 + ? Number((weightedRating / totalRatings).toFixed(1)) + : 0, + bestRating: MAX_RATING_VALUE, + worstRating: MIN_RATING_VALUE, + }; + + return aggregateRating; }; export const toReview = (review: Review): CommerceReview => ({ @@ -33,8 +75,11 @@ export const toReview = (review: Review): CommerceReview => ({ ], datePublished: review.review_date, reviewBody: review.review, + dateCreated: review.order_date, reviewRating: { "@type": "AggregateRating", ratingValue: Number(review.rate), + // this api does not support multiple reviews + reviewCount: 1, }, }); diff --git a/verified-reviews/utils/types.ts b/verified-reviews/utils/types.ts index 3a47c48ba..e48d57f04 100644 --- a/verified-reviews/utils/types.ts +++ b/verified-reviews/utils/types.ts @@ -1,5 +1,5 @@ import { - AggregateRating, + AggregateRating as CommerceAggregateRating, Review as CommerceReview, } from "../../commerce/types.ts"; @@ -42,10 +42,14 @@ export interface Review { export interface Reviews { reviews: Review[]; - status: number[]; + stats: number[]; +} + +export interface AggregateRating extends CommerceAggregateRating { + stats?: number[]; } export interface VerifiedReviewsFullReview { - aggregateRating?: AggregateRating; review: CommerceReview[]; + aggregateRating?: AggregateRating; }