Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to aggregate similar products to verified reviews #634

2 changes: 2 additions & 0 deletions commerce/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ export interface Review extends Omit<Thing, "@type"> {
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. */
Expand Down
21 changes: 15 additions & 6 deletions verified-reviews/loaders/productDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -17,18 +20,24 @@ export default function productDetailsPage(
ctx: AppContext,
): ExtensionOf<ProductDetailsPage | null> {
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,
Expand Down
33 changes: 33 additions & 0 deletions verified-reviews/loaders/productReviews.ts
Original file line number Diff line number Diff line change
@@ -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<Review[] | null> {
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) ?? [];
}
6 changes: 4 additions & 2 deletions verified-reviews/manifest.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
81 changes: 46 additions & 35 deletions verified-reviews/utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createClient>;
export interface PaginationOptions {
Expand All @@ -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",
Expand Down Expand Up @@ -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<Reviews>(`${baseUrl}`, {

return fetchAPI<Reviews[]>(`${baseUrl}`, {
method: "POST",
body: JSON.stringify(payload),
});
};
const fullReview = async (
{ productId, count = 5, offset = 0 }: PaginationOptions & {
productId: string;
},
): Promise<VerifiedReviewsFullReview> => {

const fullReview = async ({
productId,
count = 5,
offset = 0,
order,
}: PaginationOptions & {
productId: string | string[];
}): Promise<VerifiedReviewsFullReview> => {
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) {
Expand Down
47 changes: 46 additions & 1 deletion verified-reviews/utils/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 => ({
Expand All @@ -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,
},
});
10 changes: 7 additions & 3 deletions verified-reviews/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
AggregateRating,
AggregateRating as CommerceAggregateRating,
Review as CommerceReview,
} from "../../commerce/types.ts";

Expand Down Expand Up @@ -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;
}
Loading