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';
|
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 { 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} />;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
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,
|
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;
|
||||||
|
}
|
||||||
|
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;
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user