Skip to content

Commit

Permalink
feat: add option to aggregate similar products to verified reviews (#634
Browse files Browse the repository at this point in the history
)

* feat: create verified reviews loader and fix types

* fix: loader problem when product has no reviews

* feat: add option to aggregate similar products reviews

* revert order type

* feat: add data to strucutred data
  • Loading branch information
pedrobernardina authored Dec 17, 2024
1 parent 74cbc95 commit bff2a88
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 47 deletions.
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;
}

0 comments on commit bff2a88

Please sign in to comment.