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 voucher validation #52

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ActionFunction, ActionFunctionArgs } from '@remix-run/node';
import { getStoreFront } from '~/use-cases/storefront.server';
import { privateJson } from '~/core/bridge/privateJson.server';
import { getContext } from '~/use-cases/http/utils';
import { validateVoucher } from '~/use-cases/crystallize/read/validateVoucher.server';

export const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
const requestContext = getContext(request);
const { secret: storefront } = await getStoreFront(requestContext.host);
const body = await request.json();

const checkVoucher = await validateVoucher(body.voucher, {
apiClient: storefront.apiClient,
});

return privateJson({
isValid: checkVoucher,
});
};
4 changes: 3 additions & 1 deletion shared/application/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"immutable": "You have initiated, finalized or cancelled a payment, this version of your cart is therefore locked.",
"clone": "Clone previous cart",
"voucherCode": "Voucher code",
"useVoucher": "Use voucher"
"useVoucher": "Use voucher",
"voucherApplied": "Voucher applied.",
"voucherInvalid": "Voucher is invalid."
},
"dimensions": "Dimensions",
"specifications": "Manuals and specifications",
Expand Down
4 changes: 3 additions & 1 deletion shared/application/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"immutable": "Vous avez initié, finalisé ou annulé un paiement, cette version de votre caddie est donc bloquée.",
"clone": "Cloner le caddie précédent",
"voucherCode": "Bon de réduction",
"useVoucher": "Utiliser bon de réduction"
"useVoucher": "Utiliser bon de réduction",
"voucherApplied": "Bon de réduction appliqué.",
"voucherInvalid": "Le bon de réduction est invalide."
},
"dimensions": "Dimensions",
"specifications": "Manuels et caractéristiques",
Expand Down
4 changes: 3 additions & 1 deletion shared/application/translations/no.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"immutable": "Du har igangsatt, avsluttet eller kansellert en betaling, denne versjonen av handlekurven din er derfor låst.",
"clone": "Klone forrige handlekurv",
"voucherCode": "Rabatt code",
"useVoucher": "Bruk rabatt"
"useVoucher": "Bruk rabatt",
"voucherApplied": "Rabatt brukt.",
"voucherInvalid": "Rabattkoden er ugyldig."
},
"dimensions": "Dimensjoner",
"specifications": "Manualer og spesifikasjoner",
Expand Down
15 changes: 14 additions & 1 deletion shared/application/ui/components/voucher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ import { useRemoteCart } from '../hooks/useRemoteCart';
import { Input } from './input';

export const VoucherForm: React.FC = () => {
const { cart: localCart, setVoucher } = useLocalCart();
const { cart: localCart, setVoucher, validateVoucher } = useLocalCart();
const { loading } = useRemoteCart();
const [voucherValue, setVoucherValue] = useState(localCart?.extra?.voucher ?? '');
const [showMessage, setShowMessage] = useState('');

const { _t } = useAppContext();

const checkVoucher = async (voucher: string) => {
if (!voucher) return;
const checkVoucher = await validateVoucher(voucher);
return checkVoucher.isValid
? setShowMessage(_t('cart.voucherApplied'))
: setShowMessage(_t('cart.voucherInvalid'));
};

return (
<ClientOnly>
<div className="flex flex-col w-full">
Expand All @@ -33,6 +43,7 @@ export const VoucherForm: React.FC = () => {
className="bg-[#000] text-[#fff] px-2 py-1 rounded mt-5 text-center h-10"
onClick={() => {
setVoucher(voucherValue);
checkVoucher(voucherValue);
}}
>
{loading && localCart?.extra?.voucher ? _t('loading') : _t('cart.useVoucher')}
Expand All @@ -44,12 +55,14 @@ export const VoucherForm: React.FC = () => {
onClick={() => {
setVoucherValue('');
setVoucher('');
setShowMessage('');
}}
>
{_t('delete')}
</button>
)}
</div>
{showMessage && <p className="text-sm mt-2 text-[#666]">{showMessage}</p>}
</div>
</ClientOnly>
);
Expand Down
11 changes: 11 additions & 0 deletions shared/application/ui/hooks/useLocalCart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';
import { useLocalStorage, writeStorage } from '@rehooks/local-storage';
import { LocalCart } from '~/use-cases/contracts/LocalCart';
import { ServiceAPI } from '~/use-cases/service-api';
import { useAppContext } from '../app-context/provider';

const InitializeEmptyLocalCart = (): LocalCart => {
return {
Expand All @@ -18,6 +20,7 @@ export function useLocalCart() {
...cart,
});
};
const { state: appContextState } = useAppContext();

const isImmutable = () => {
return cart.state === 'placed';
Expand Down Expand Up @@ -106,5 +109,13 @@ export function useLocalCart() {
},
});
},
validateVoucher: async (voucher: string) => {
const api = ServiceAPI({
language: appContextState.language,
serviceApiUrl: appContextState.serviceApiUrl,
});

return await api.validateVoucher(voucher);
},
};
}
10 changes: 10 additions & 0 deletions shared/application/use-cases/contracts/RemoteCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,14 @@ export type Cart = {
};
state: 'cart' | 'placed' | 'ordered';
orderId?: string;
context?: {
price: {
markets?: string[];
decimals: number;
selectedVariantIdentifier: string;
fallbackVariantIdentifiers: string[];
compareAtVariantIdentifier: string;
voucherCode?: string;
};
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ClientInterface } from '@crystallize/js-api-client';
import { jsonToGraphQLQuery } from 'json-to-graphql-query';

type Deps = {
apiClient: ClientInterface;
};

export const validateVoucher = async (voucher: string, { apiClient }: Deps) => {
const query = {
validateVoucher: {
__args: {
voucher,
},
isValid: true,
},
};

const rawQuery = jsonToGraphQLQuery({ query });

try {
const response = await apiClient.shopCartApi(rawQuery);
return response.validateVoucher.isValid;
} catch (exception: any) {
console.error('Failed to fetch cart', exception.message);
return {
valid: false,
};
}
};
3 changes: 2 additions & 1 deletion shared/application/use-cases/crystallize/write/editCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ type Deps = {

type Input = {
items: Array<{ sku: string; quantity: number }>;
context: Record<string, unknown>;
context: Cart['context'];
id?: string;
};

export const hydrateCart = async (
items: Array<{ sku: string; quantity: number }>,
{ apiClient }: Deps,
Expand Down
4 changes: 4 additions & 0 deletions shared/application/use-cases/service-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export const ServiceAPI = ({ locale, language, serviceApiUrl }: ServiceAPIContex
withImages: true,
extra: cart.extra,
}),
validateVoucher: (voucher: string) =>
postJson<any>(serviceApiUrl + '/cart/voucher', {
voucher,
}),
// THIS SHOULD BE REMOVED IN A REAL PROJECT
sendPaidOrderWithCrystalCoin: (cart: LocalCart, customer: Partial<Customer>) =>
sendPaidOrderWithCrystalCoin(serviceApiUrl, language, cart, customer),
Expand Down
Loading