mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 23:46:58 +00:00
Update : discount calculation on cart fetching
This commit is contained in:
parent
828add1aa0
commit
11b8711fca
@ -81,3 +81,55 @@ export async function updateItemQuantity(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import { calculateDiscounts } from './actions';
|
||||
import CartModal from './modal';
|
||||
|
||||
export default async function Cart() {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
let cart;
|
||||
|
||||
let tieredDiscounts;
|
||||
if (cartId) {
|
||||
cart = await getCart(cartId);
|
||||
tieredDiscounts = await calculateDiscounts(cart);
|
||||
}
|
||||
|
||||
return <CartModal cart={cart} />;
|
||||
return <CartModal cart={cart} tieredDiscount={tieredDiscounts} />;
|
||||
}
|
||||
|
@ -13,60 +13,19 @@ import CloseCart from './close-cart';
|
||||
import { DeleteItemButton } from './delete-item-button';
|
||||
import { EditItemQuantityButton } from './edit-item-quantity-button';
|
||||
import OpenCart from './open-cart';
|
||||
import TierDiscount from './tier-discount';
|
||||
|
||||
type MerchandiseSearchParams = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
function TierDiscount({ subTotal }: { subTotal: number }) {
|
||||
const discountGroups = [
|
||||
{
|
||||
name: 'Tier 1',
|
||||
discount: {
|
||||
amount: 0.05,
|
||||
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 }) {
|
||||
export default function CartModal({
|
||||
cart,
|
||||
tieredDiscount
|
||||
}: {
|
||||
cart: Cart | undefined;
|
||||
tieredDiscount: any;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const quantityRef = useRef(cart?.totalQuantity);
|
||||
const openCart = () => setIsOpen(true);
|
||||
@ -127,7 +86,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{cart.lines.map((item, i) => {
|
||||
const merchandiseSearchParams = {} as MerchandiseSearchParams;
|
||||
|
66
components/cart/tier-discount.tsx
Normal file
66
components/cart/tier-discount.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
11
lib/shopify/fragments/discount-metaobject.ts
Normal file
11
lib/shopify/fragments/discount-metaobject.ts
Normal file
@ -0,0 +1,11 @@
|
||||
const discountMetaobject = /* GraphQL */ `
|
||||
fragment metaobject on Metaobject {
|
||||
fields {
|
||||
value
|
||||
key
|
||||
}
|
||||
handle
|
||||
}
|
||||
`;
|
||||
|
||||
export default discountMetaobject;
|
@ -16,6 +16,7 @@ import {
|
||||
getCollectionQuery,
|
||||
getCollectionsQuery
|
||||
} from './queries/collection';
|
||||
import { getDiscountMetaobjectsQuery } from './queries/discount-metaobject';
|
||||
import { getMenuQuery } from './queries/menu';
|
||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||
import {
|
||||
@ -39,6 +40,7 @@ import {
|
||||
ShopifyCollectionProductsOperation,
|
||||
ShopifyCollectionsOperation,
|
||||
ShopifyCreateCartOperation,
|
||||
ShopifyDiscountMetaobjectsOperation,
|
||||
ShopifyMenuOperation,
|
||||
ShopifyPageOperation,
|
||||
ShopifyPagesOperation,
|
||||
@ -448,3 +450,12 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||
|
||||
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;
|
||||
}
|
||||
|
12
lib/shopify/queries/discount-metaobject.ts
Normal file
12
lib/shopify/queries/discount-metaobject.ts
Normal 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}
|
||||
`;
|
@ -61,6 +61,19 @@ export type Page = {
|
||||
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'> & {
|
||||
variants: ProductVariant[];
|
||||
images: Image[];
|
||||
@ -263,3 +276,14 @@ export type ShopifyProductsOperation = {
|
||||
sortKey?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyDiscountMetaobjectsOperation = {
|
||||
data: {
|
||||
metaobjects: Connection<DiscountMetaobject>;
|
||||
};
|
||||
variables: {
|
||||
query?: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user