Update : discount calculation on cart fetching

This commit is contained in:
Wijayaac 2024-03-27 19:34:16 +07:00
parent 828add1aa0
commit 11b8711fca
8 changed files with 189 additions and 52 deletions

View File

@ -81,3 +81,55 @@ export async function updateItemQuantity(
return 'Error updating item quantity'; return 'Error updating item quantity';
} }
} }
export async function calculateDiscounts(cart: any) {
const discountGroups = [
{
name: 'Tier 2',
discount: {
amount: 0.1,
minimumSpent: 150
}
},
{
name: 'Tier 1',
discount: {
amount: 0.05,
minimumSpent: 120
}
}
];
const subTotal = cart?.cost.subtotalAmount.amount;
const currencyCode = cart?.cost.subtotalAmount.currencyCode;
const discountGroupsSorted = discountGroups.sort(
(a, b) => a.discount.minimumSpent - b.discount.minimumSpent
);
const minSpent = Math.max(...discountGroupsSorted.map((group) => group.discount.minimumSpent));
const eligibleDiscount = discountGroupsSorted.filter(
(group) => subTotal >= group.discount.minimumSpent
);
const finalDiscount = eligibleDiscount.length
? Math.max(...eligibleDiscount.map((group) => group.discount.amount))
: 0;
const closestNextTier = discountGroupsSorted
.filter((group) => group.discount.minimumSpent > subTotal)
.shift();
const spentToNextDiscount = closestNextTier
? closestNextTier?.discount.minimumSpent - subTotal
: 0;
const nextDiscount = closestNextTier?.discount.amount;
const discountAmount = finalDiscount ? finalDiscount * 100 : 0;
return {
discountAmount,
spentToNextDiscount,
nextDiscount,
discountGroups: discountGroupsSorted,
minSpent,
subTotal,
currencyCode
};
}

View File

@ -1,14 +1,16 @@
import { getCart } from 'lib/shopify'; import { getCart } from 'lib/shopify';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { calculateDiscounts } from './actions';
import CartModal from './modal'; import CartModal from './modal';
export default async function Cart() { export default async function Cart() {
const cartId = cookies().get('cartId')?.value; const cartId = cookies().get('cartId')?.value;
let cart; let cart;
let tieredDiscounts;
if (cartId) { if (cartId) {
cart = await getCart(cartId); cart = await getCart(cartId);
tieredDiscounts = await calculateDiscounts(cart);
} }
return <CartModal cart={cart} />; return <CartModal cart={cart} tieredDiscount={tieredDiscounts} />;
} }

View File

@ -13,60 +13,19 @@ import CloseCart from './close-cart';
import { DeleteItemButton } from './delete-item-button'; import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button'; import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart'; import OpenCart from './open-cart';
import TierDiscount from './tier-discount';
type MerchandiseSearchParams = { type MerchandiseSearchParams = {
[key: string]: string; [key: string]: string;
}; };
function TierDiscount({ subTotal }: { subTotal: number }) { export default function CartModal({
const discountGroups = [ cart,
{ tieredDiscount
name: 'Tier 1', }: {
discount: { cart: Cart | undefined;
amount: 0.05, tieredDiscount: any;
minimumSpent: 10 }) {
}
},
{
name: 'Tier 2',
discount: {
amount: 0.1,
minimumSpent: 20
}
}
];
const highestMinimumSpent = discountGroups.reduce((acc, group) => {
if (group.discount.minimumSpent > acc) {
return group.discount.minimumSpent;
} else {
return acc;
}
}, 0);
const eligibleDiscountGroups = discountGroups.filter(
(group) => subTotal >= group.discount.minimumSpent
);
const finalDiscount = eligibleDiscountGroups.reduce((prev, current) => {
// Compare and return the highest discount group based on your criteria
return prev.discount.minimumSpent > current.discount.minimumSpent ? prev : current;
});
const discountAmount = finalDiscount.discount.amount * 100;
return (
<div className="flex flex-col items-center justify-between border-b border-neutral-200 pb-1 dark:border-neutral-700">
<p>Spent more</p>
<div className="h-2.5 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-2.5 rounded-full bg-blue-600"
style={{ width: `${(subTotal / highestMinimumSpent) * 100}%` }}
></div>
</div>
<p>{discountAmount}% off</p>
</div>
);
}
export default function CartModal({ cart }: { cart: Cart | undefined }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const quantityRef = useRef(cart?.totalQuantity); const quantityRef = useRef(cart?.totalQuantity);
const openCart = () => setIsOpen(true); const openCart = () => setIsOpen(true);
@ -127,7 +86,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
</div> </div>
) : ( ) : (
<div className="flex h-full flex-col justify-between overflow-hidden p-1"> <div className="flex h-full flex-col justify-between overflow-hidden p-1">
<TierDiscount subTotal={Number(cart?.cost.subtotalAmount.amount)} /> <TierDiscount tieredDiscount={tieredDiscount} />
<ul className="flex-grow overflow-auto py-4"> <ul className="flex-grow overflow-auto py-4">
{cart.lines.map((item, i) => { {cart.lines.map((item, i) => {
const merchandiseSearchParams = {} as MerchandiseSearchParams; const merchandiseSearchParams = {} as MerchandiseSearchParams;

View File

@ -0,0 +1,66 @@
import Price from 'components/price';
type DiscountGroup = {
discount: {
amount: number;
minimumSpent: number;
};
};
export default function TierDiscount({ tieredDiscount }: { tieredDiscount: any }) {
if (!tieredDiscount) {
return;
}
const {
discountGroups,
discountAmount,
spentToNextDiscount,
minSpent,
nextDiscount,
subTotal,
currencyCode
} = tieredDiscount;
return (
<>
<div className="mt-2 flex flex-col items-center justify-between gap-2 border-b border-neutral-200 pb-4 md:mt-4 dark:border-neutral-700">
<div className="flex gap-2">
{spentToNextDiscount > 0 && (
<>
Spent more than
<Price
className="text-right text-base text-black dark:text-white"
amount={JSON.stringify(spentToNextDiscount)}
currencyCode={currencyCode}
/>
</>
)}
{nextDiscount && <span>to get {nextDiscount * 100}%</span>}
</div>
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-2.5 rounded-full bg-blue-600"
style={{ width: `${(subTotal / minSpent) * 100}%` }}
/>
{discountGroups.map((group: DiscountGroup, i: number) => (
<div
key={i}
className={`absolute top-0 h-2.5 rounded-full bg-blue-${
i === 0 ? 400 : 200
} dark:bg-blue-${i === 0 ? 400 : 700}`}
style={{ width: `${(group.discount.minimumSpent / minSpent) * 100}%` }}
/>
))}
</div>
<p className="text-base text-black dark:text-white">
{spentToNextDiscount === 0 ? (
<span>Congratulations you got {discountAmount}% Discounts!</span>
) : (
<span>You got : {discountAmount}% off</span>
)}
</p>
</div>
</>
);
}

View File

@ -0,0 +1,11 @@
const discountMetaobject = /* GraphQL */ `
fragment metaobject on Metaobject {
fields {
value
key
}
handle
}
`;
export default discountMetaobject;

View File

@ -16,6 +16,7 @@ import {
getCollectionQuery, getCollectionQuery,
getCollectionsQuery getCollectionsQuery
} from './queries/collection'; } from './queries/collection';
import { getDiscountMetaobjectsQuery } from './queries/discount-metaobject';
import { getMenuQuery } from './queries/menu'; import { getMenuQuery } from './queries/menu';
import { getPageQuery, getPagesQuery } from './queries/page'; import { getPageQuery, getPagesQuery } from './queries/page';
import { import {
@ -39,6 +40,7 @@ import {
ShopifyCollectionProductsOperation, ShopifyCollectionProductsOperation,
ShopifyCollectionsOperation, ShopifyCollectionsOperation,
ShopifyCreateCartOperation, ShopifyCreateCartOperation,
ShopifyDiscountMetaobjectsOperation,
ShopifyMenuOperation, ShopifyMenuOperation,
ShopifyPageOperation, ShopifyPageOperation,
ShopifyPagesOperation, ShopifyPagesOperation,
@ -448,3 +450,12 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() }); return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
} }
export async function getDiscountMetaobjects() {
const res = await shopifyFetch<ShopifyDiscountMetaobjectsOperation>({
query: getDiscountMetaobjectsQuery,
cache: 'no-store'
});
return res.body.data.metaobjects;
}

View File

@ -0,0 +1,12 @@
import discountMetaobject from '../fragments/discount-metaobject';
export const getDiscountMetaobjectsQuery = /* GraphQL */ `
query getDiscountMetaobjects {
metaobjects(type: "dynamic_discount", first: 10) {
nodes {
...metaobject
}
}
}
${discountMetaobject}
`;

View File

@ -61,6 +61,19 @@ export type Page = {
updatedAt: string; updatedAt: string;
}; };
type Field = {
key: string;
value: string;
};
type MetaObject = {
fields: Field[];
};
export type DiscountMetaobject = MetaObject & {
handle: string;
};
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & { export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
variants: ProductVariant[]; variants: ProductVariant[];
images: Image[]; images: Image[];
@ -263,3 +276,14 @@ export type ShopifyProductsOperation = {
sortKey?: string; sortKey?: string;
}; };
}; };
export type ShopifyDiscountMetaobjectsOperation = {
data: {
metaobjects: Connection<DiscountMetaobject>;
};
variables: {
query?: string;
reverse?: boolean;
sortKey?: string;
};
};