fix: Add SALEOR_API_TOKEN auth for COD transaction creation

- Add SALEOR_API_TOKEN environment variable support
- Update Apollo client to include auth header
- Enable COD transaction creation after checkout
This commit is contained in:
Unchained
2026-03-29 18:22:16 +02:00
parent 5f9b7bac3a
commit e15e6470d2
7 changed files with 921 additions and 173 deletions

View File

@@ -13,18 +13,13 @@ import { saleorClient } from "@/lib/saleor/client";
import { useAnalytics } from "@/lib/analytics";
import {
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE,
CHECKOUT_EMAIL_UPDATE,
CHECKOUT_METADATA_UPDATE,
CHECKOUT_SHIPPING_METHOD_UPDATE,
ORDER_METADATA_UPDATE,
CHECKOUT_LANGUAGE_CODE_UPDATE,
TRANSACTION_CREATE,
} from "@/lib/saleor/mutations/Checkout";
import { PaymentSection } from "./components/PaymentSection";
import { DEFAULT_PAYMENT_METHOD } from "@/lib/config/paymentMethods";
import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout";
import type { Checkout } from "@/types/saleor";
import { createCheckoutService, type Address } from "@/lib/services/checkoutService";
interface ShippingAddressUpdateResponse {
checkoutShippingAddressUpdate?: {
@@ -33,44 +28,6 @@ interface ShippingAddressUpdateResponse {
};
}
interface BillingAddressUpdateResponse {
checkoutBillingAddressUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutCompleteResponse {
checkoutComplete?: {
order?: { id: string; number: string };
errors?: Array<{ message: string }>;
};
}
interface EmailUpdateResponse {
checkoutEmailUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface MetadataUpdateResponse {
updateMetadata?: {
item?: {
id: string;
metadata?: Array<{ key: string; value: string }>;
};
errors?: Array<{ message: string }>;
};
}
interface ShippingMethodUpdateResponse {
checkoutShippingMethodUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutQueryResponse {
checkout?: Checkout;
}
@@ -100,7 +57,7 @@ export default function CheckoutPage() {
const t = useTranslations("Checkout");
const locale = useLocale();
const router = useRouter();
const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore();
const { checkout, refreshCheckout, clearCheckout, getLines, getTotal } = useSaleorCheckoutStore();
const { trackCheckoutStarted, trackCheckoutStep, trackOrderCompleted, identifyUser } = useAnalytics();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -291,137 +248,112 @@ export default function CheckoutPage() {
setError(null);
try {
console.log("Completing order...");
console.log("Completing order via CheckoutService...");
console.log("Step 1: Updating email...");
const emailResult = await saleorClient.mutate<EmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE,
variables: {
checkoutId: checkout.id,
email: shippingAddress.email,
},
});
// Create checkout service instance
const checkoutService = createCheckoutService(checkout.id);
if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) {
const errorMessage = emailResult.data.checkoutEmailUpdate.errors[0].message;
if (errorMessage.includes("Couldn't resolve to a node")) {
console.error("Checkout not found, clearing cart...");
localStorage.removeItem('cart');
localStorage.removeItem('checkoutId');
window.location.href = `/${locale}/products`;
return;
}
throw new Error(`Email update failed: ${errorMessage}`);
// Transform form data to service types
const serviceShippingAddress: Address = {
firstName: shippingAddress.firstName,
lastName: shippingAddress.lastName,
streetAddress1: shippingAddress.streetAddress1,
streetAddress2: shippingAddress.streetAddress2,
city: shippingAddress.city,
postalCode: shippingAddress.postalCode,
country: shippingAddress.country,
phone: shippingAddress.phone,
};
const serviceBillingAddress: Address = {
firstName: billingAddress.firstName,
lastName: billingAddress.lastName,
streetAddress1: billingAddress.streetAddress1,
streetAddress2: billingAddress.streetAddress2,
city: billingAddress.city,
postalCode: billingAddress.postalCode,
country: billingAddress.country,
phone: billingAddress.phone,
};
// Execute checkout pipeline
const result = await checkoutService.execute({
email: shippingAddress.email,
shippingAddress: serviceShippingAddress,
billingAddress: serviceBillingAddress,
shippingMethodId: selectedShippingMethod,
languageCode: locale.toUpperCase(),
metadata: {
phone: shippingAddress.phone,
shippingPhone: shippingAddress.phone,
userLanguage: locale,
userLocale: locale,
},
});
if (!result.success || !result.order) {
// Handle specific error types
if (result.error === "CHECKOUT_EXPIRED") {
console.error("Checkout not found, clearing cart...");
localStorage.removeItem('cart');
localStorage.removeItem('checkoutId');
window.location.href = `/${locale}/products`;
return;
}
console.log("Step 1: Email updated successfully");
throw new Error(result.error || t("errorCreatingOrder"));
}
console.log("Step 2: Updating language code...");
await saleorClient.mutate({
mutation: CHECKOUT_LANGUAGE_CODE_UPDATE,
variables: {
checkoutId: checkout.id,
languageCode: locale.toUpperCase(),
},
});
console.log("Step 2: Language code updated to", locale.toUpperCase());
console.log("Step 3: Updating billing address...");
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
billingAddress: {
firstName: billingAddress.firstName,
lastName: billingAddress.lastName,
streetAddress1: billingAddress.streetAddress1,
streetAddress2: billingAddress.streetAddress2,
city: billingAddress.city,
postalCode: billingAddress.postalCode,
country: billingAddress.country,
phone: billingAddress.phone,
},
},
});
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
throw new Error(`Billing address update failed: ${billingResult.data.checkoutBillingAddressUpdate.errors[0].message}`);
}
console.log("Step 3: Billing address updated successfully");
console.log("Step 4: Setting shipping method...");
const shippingMethodResult = await saleorClient.mutate<ShippingMethodUpdateResponse>({
mutation: CHECKOUT_SHIPPING_METHOD_UPDATE,
variables: {
checkoutId: checkout.id,
shippingMethodId: selectedShippingMethod,
},
});
if (shippingMethodResult.data?.checkoutShippingMethodUpdate?.errors && shippingMethodResult.data.checkoutShippingMethodUpdate.errors.length > 0) {
throw new Error(`Shipping method update failed: ${shippingMethodResult.data.checkoutShippingMethodUpdate.errors[0].message}`);
}
console.log("Step 4: Shipping method set successfully");
console.log("Step 5: Saving metadata...");
const metadataResult = await saleorClient.mutate<MetadataUpdateResponse>({
mutation: CHECKOUT_METADATA_UPDATE,
variables: {
checkoutId: checkout.id,
metadata: [
{ key: "phone", value: shippingAddress.phone },
{ key: "shippingPhone", value: shippingAddress.phone },
{ key: "userLanguage", value: locale },
{ key: "userLocale", value: locale },
],
},
});
if (metadataResult.data?.updateMetadata?.errors && metadataResult.data.updateMetadata.errors.length > 0) {
console.warn("Failed to save phone metadata:", metadataResult.data.updateMetadata.errors);
} else {
console.log("Step 5: Phone number saved successfully");
}
console.log("Step 6: Completing checkout...");
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
mutation: CHECKOUT_COMPLETE,
variables: {
checkoutId: checkout.id,
},
});
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
}
const order = completeResult.data?.checkoutComplete?.order;
if (order) {
setOrderNumber(order.number);
setOrderComplete(true);
// Track order completion
const lines = getLines();
const total = getTotal();
trackOrderCompleted({
order_id: checkout.id,
order_number: order.number,
total,
currency: "RSD",
item_count: lines.reduce((sum, line) => sum + line.quantity, 0),
shipping_cost: shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount,
customer_email: shippingAddress.email,
// Success!
setOrderNumber(result.order.number);
setOrderComplete(true);
// If COD payment, create a transaction on the order
if (selectedPaymentMethod === 'cod' && result.order.id) {
try {
console.log("Creating COD transaction for order:", result.order.id);
await saleorClient.mutate({
mutation: TRANSACTION_CREATE,
variables: {
orderId: result.order.id,
transaction: {
name: "Cash on Delivery",
message: "COD - Payment pending on delivery"
}
}
});
// Identify the user
identifyUser({
profileId: shippingAddress.email,
email: shippingAddress.email,
firstName: shippingAddress.firstName,
lastName: shippingAddress.lastName,
});
} else {
throw new Error(t("errorCreatingOrder"));
console.log("COD transaction created successfully");
} catch (txError) {
console.error("Failed to create COD transaction:", txError);
// Don't fail the checkout if transaction creation fails
}
}
// Clear the checkout/cart from the store
clearCheckout();
// Track order completion
const lines = getLines();
const total = getTotal();
trackOrderCompleted({
order_id: checkout.id,
order_number: result.order.number,
total,
currency: "RSD",
item_count: lines.reduce((sum, line) => sum + line.quantity, 0),
shipping_cost: shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount,
customer_email: shippingAddress.email,
});
// Identify the user
identifyUser({
profileId: shippingAddress.email,
email: shippingAddress.email,
firstName: shippingAddress.firstName,
lastName: shippingAddress.lastName,
});
console.log("Order completed successfully:", result.order.number);
} catch (err: unknown) {
console.error("Checkout error:", err);

View File

@@ -6,12 +6,13 @@ const httpLink = createHttpLink({
});
const authLink = setContext((_, { headers }) => {
// Saleor doesn't require auth for public queries
// Add auth token here if needed for admin operations
// Add auth token for admin operations
const token = process.env.SALEOR_API_TOKEN;
return {
headers: {
...headers,
"Content-Type": "application/json",
...(token && { "Authorization": `Bearer ${token}` }),
},
};
});

View File

@@ -210,3 +210,17 @@ export const CHECKOUT_LANGUAGE_CODE_UPDATE = gql`
}
}
`;
export const TRANSACTION_CREATE = gql`
mutation CreateTransaction($orderId: ID!, $transaction: TransactionCreateInput!) {
transactionCreate(id: $orderId, transaction: $transaction) {
transaction {
id
}
errors {
field
message
}
}
}
`;

View File

@@ -0,0 +1,482 @@
/**
* Checkout Service - Domain layer for checkout operations
*
* This module encapsulates all checkout business logic, making it:
* - Testable: Pure functions with no UI dependencies
* - Composable: Steps can be chained, mocked, or replaced
* - Type-safe: All inputs/outputs are strictly typed
* - Resilient: Clear contracts prevent ordering mistakes
*/
import { saleorClient } from "@/lib/saleor/client";
import type { Checkout, CheckoutLine } from "@/types/saleor";
import {
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE,
CHECKOUT_EMAIL_UPDATE,
CHECKOUT_METADATA_UPDATE,
CHECKOUT_SHIPPING_METHOD_UPDATE,
CHECKOUT_LANGUAGE_CODE_UPDATE,
} from "@/lib/saleor/mutations/Checkout";
// ============================================================================
// GraphQL Response Types
// ============================================================================
interface GraphQLError {
field?: string;
message: string;
code?: string;
}
interface CheckoutEmailUpdateResponse {
checkoutEmailUpdate?: {
checkout?: Checkout;
errors?: GraphQLError[];
};
}
interface CheckoutLanguageCodeUpdateResponse {
checkoutLanguageCodeUpdate?: {
checkout?: { id: string; languageCode: string };
errors?: GraphQLError[];
};
}
interface CheckoutShippingAddressUpdateResponse {
checkoutShippingAddressUpdate?: {
checkout?: Checkout;
errors?: GraphQLError[];
};
}
interface CheckoutBillingAddressUpdateResponse {
checkoutBillingAddressUpdate?: {
checkout?: Checkout;
errors?: GraphQLError[];
};
}
interface CheckoutShippingMethodUpdateResponse {
checkoutShippingMethodUpdate?: {
checkout?: Checkout;
errors?: GraphQLError[];
};
}
interface CheckoutMetadataUpdateResponse {
updateMetadata?: {
item?: {
id: string;
metadata?: Array<{ key: string; value: string }>;
};
errors?: GraphQLError[];
};
}
interface CheckoutCompleteResponse {
checkoutComplete?: {
order?: {
id: string;
number: string;
status: string;
created: string;
total?: {
gross: {
amount: number;
currency: string;
};
};
};
errors?: GraphQLError[];
};
}
// ============================================================================
// Domain Types
// ============================================================================
export interface Address {
firstName: string;
lastName: string;
streetAddress1: string;
streetAddress2?: string;
city: string;
postalCode: string;
country: string;
phone: string;
}
export interface CheckoutInput {
checkoutId: string;
email: string;
shippingAddress: Address;
billingAddress: Address;
shippingMethodId: string;
languageCode: string;
metadata: Record<string, string>;
}
export interface CheckoutResult {
success: boolean;
order?: {
id: string;
number: string;
languageCode: string;
};
error?: string;
}
export interface CheckoutStepResult<T = unknown> {
success: boolean;
data?: T;
error?: string;
}
// ============================================================================
// Individual Checkout Steps (Composable Units)
// ============================================================================
/**
* Step 1: Update checkout email
* Isolated, testable unit that does one thing
*/
export async function updateCheckoutEmail(
checkoutId: string,
email: string
): Promise<CheckoutStepResult> {
const { data } = await saleorClient.mutate<CheckoutEmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE,
variables: { checkoutId, email },
});
if (data?.checkoutEmailUpdate?.errors?.length) {
const error = data.checkoutEmailUpdate.errors[0];
if (error.message.includes("Couldn't resolve to a node")) {
return { success: false, error: "CHECKOUT_EXPIRED" };
}
return { success: false, error: error.message };
}
return { success: true };
}
/**
* Step 2: Update language code
* CRITICAL: Must be called before checkoutComplete for correct email language
*/
export async function updateCheckoutLanguage(
checkoutId: string,
languageCode: string
): Promise<CheckoutStepResult> {
const { data } = await saleorClient.mutate<CheckoutLanguageCodeUpdateResponse>({
mutation: CHECKOUT_LANGUAGE_CODE_UPDATE,
variables: { checkoutId, languageCode: languageCode.toUpperCase() },
});
if (data?.checkoutLanguageCodeUpdate?.errors?.length) {
return { success: false, error: data.checkoutLanguageCodeUpdate.errors[0].message };
}
return { success: true };
}
/**
* Step 3: Update shipping address
*/
export async function updateShippingAddress(
checkoutId: string,
address: Address
): Promise<CheckoutStepResult> {
const { data } = await saleorClient.mutate<CheckoutShippingAddressUpdateResponse>({
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
variables: {
checkoutId,
shippingAddress: {
firstName: address.firstName,
lastName: address.lastName,
streetAddress1: address.streetAddress1,
streetAddress2: address.streetAddress2 || "",
city: address.city,
postalCode: address.postalCode,
country: address.country,
phone: address.phone,
},
},
});
if (data?.checkoutShippingAddressUpdate?.errors?.length) {
return { success: false, error: data.checkoutShippingAddressUpdate.errors[0].message };
}
return { success: true };
}
/**
* Step 4: Update billing address
*/
export async function updateBillingAddress(
checkoutId: string,
address: Address
): Promise<CheckoutStepResult> {
const { data } = await saleorClient.mutate<CheckoutBillingAddressUpdateResponse>({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
checkoutId,
billingAddress: {
firstName: address.firstName,
lastName: address.lastName,
streetAddress1: address.streetAddress1,
streetAddress2: address.streetAddress2 || "",
city: address.city,
postalCode: address.postalCode,
country: address.country,
phone: address.phone,
},
},
});
if (data?.checkoutBillingAddressUpdate?.errors?.length) {
return { success: false, error: data.checkoutBillingAddressUpdate.errors[0].message };
}
return { success: true };
}
/**
* Step 5: Update shipping method
*/
export async function updateShippingMethod(
checkoutId: string,
shippingMethodId: string
): Promise<CheckoutStepResult> {
const { data } = await saleorClient.mutate<CheckoutShippingMethodUpdateResponse>({
mutation: CHECKOUT_SHIPPING_METHOD_UPDATE,
variables: { checkoutId, shippingMethodId },
});
if (data?.checkoutShippingMethodUpdate?.errors?.length) {
return { success: false, error: data.checkoutShippingMethodUpdate.errors[0].message };
}
return { success: true };
}
/**
* Step 6: Update metadata
* Non-critical - failures are logged but don't stop checkout
*/
export async function updateCheckoutMetadata(
checkoutId: string,
metadata: Record<string, string>
): Promise<CheckoutStepResult> {
const metadataArray = Object.entries(metadata).map(([key, value]) => ({ key, value }));
const { data } = await saleorClient.mutate<CheckoutMetadataUpdateResponse>({
mutation: CHECKOUT_METADATA_UPDATE,
variables: { checkoutId, metadata: metadataArray },
});
if (data?.updateMetadata?.errors?.length) {
// Metadata is non-critical, log but don't fail
console.warn("Failed to save checkout metadata:", data.updateMetadata.errors);
return { success: true }; // Still return success
}
return { success: true };
}
/**
* Final Step: Complete checkout
* Returns the created order
*/
export async function completeCheckout(
checkoutId: string
): Promise<CheckoutStepResult<{ id: string; number: string; languageCode: string }>> {
const { data } = await saleorClient.mutate<CheckoutCompleteResponse>({
mutation: CHECKOUT_COMPLETE,
variables: { checkoutId },
});
if (data?.checkoutComplete?.errors?.length) {
return { success: false, error: data.checkoutComplete.errors[0].message };
}
const order = data?.checkoutComplete?.order;
if (!order) {
return { success: false, error: "Order creation failed - no order returned" };
}
return {
success: true,
data: {
id: order.id,
number: order.number,
languageCode: "EN", // Default fallback since checkoutComplete doesn't return languageCode directly
},
};
}
// ============================================================================
// Checkout Pipeline (Composed Steps)
// ============================================================================
/**
* Execute full checkout pipeline with proper ordering
*
* This function enforces the correct sequence of operations:
* 1. Email (identifies customer)
* 2. Language (MUST be before complete for email language!)
* 3. Addresses
* 4. Shipping method
* 5. Metadata
* 6. Complete
*
* If any step fails, the pipeline stops and returns the error.
* This prevents partial checkouts and ensures data consistency.
*/
export async function executeCheckoutPipeline(
input: CheckoutInput
): Promise<CheckoutResult> {
const { checkoutId, email, shippingAddress, billingAddress, shippingMethodId, languageCode, metadata } = input;
// Step 1: Email
const emailResult = await updateCheckoutEmail(checkoutId, email);
if (!emailResult.success) {
return { success: false, error: emailResult.error };
}
// Step 2: Language (CRITICAL for email language)
const languageResult = await updateCheckoutLanguage(checkoutId, languageCode);
if (!languageResult.success) {
return { success: false, error: languageResult.error };
}
// Step 3: Shipping Address
const shippingResult = await updateShippingAddress(checkoutId, shippingAddress);
if (!shippingResult.success) {
return { success: false, error: shippingResult.error };
}
// Step 4: Billing Address
const billingResult = await updateBillingAddress(checkoutId, billingAddress);
if (!billingResult.success) {
return { success: false, error: billingResult.error };
}
// Step 5: Shipping Method
const methodResult = await updateShippingMethod(checkoutId, shippingMethodId);
if (!methodResult.success) {
return { success: false, error: methodResult.error };
}
// Step 6: Metadata (non-critical, continues on failure)
await updateCheckoutMetadata(checkoutId, metadata);
// Step 7: Complete checkout
const completeResult = await completeCheckout(checkoutId);
if (!completeResult.success || !completeResult.data) {
return { success: false, error: completeResult.error };
}
return {
success: true,
order: completeResult.data,
};
}
// ============================================================================
// Validation Helpers
// ============================================================================
export function validateAddress(address: Partial<Address>): string | null {
if (!address.firstName?.trim()) return "First name is required";
if (!address.lastName?.trim()) return "Last name is required";
if (!address.streetAddress1?.trim()) return "Street address is required";
if (!address.city?.trim()) return "City is required";
if (!address.postalCode?.trim()) return "Postal code is required";
if (!address.country?.trim()) return "Country is required";
if (!address.phone?.trim() || address.phone.length < 8) return "Valid phone number is required";
return null;
}
export function validateEmail(email: string): string | null {
if (!email?.trim()) return "Email is required";
if (!email.includes("@")) return "Invalid email format";
return null;
}
export function validateCheckoutInput(input: CheckoutInput): string | null {
const emailError = validateEmail(input.email);
if (emailError) return emailError;
const shippingError = validateAddress(input.shippingAddress);
if (shippingError) return `Shipping ${shippingError}`;
const billingError = validateAddress(input.billingAddress);
if (billingError) return `Billing ${billingError}`;
if (!input.shippingMethodId) return "Shipping method is required";
if (!input.checkoutId) return "Checkout ID is required";
return null;
}
// ============================================================================
// Checkout Service Class (High-level API)
// ============================================================================
export class CheckoutService {
constructor(private checkoutId: string) {}
async updateEmail(email: string): Promise<CheckoutStepResult> {
return updateCheckoutEmail(this.checkoutId, email);
}
async updateLanguage(languageCode: string): Promise<CheckoutStepResult> {
return updateCheckoutLanguage(this.checkoutId, languageCode);
}
async updateShippingAddress(address: Address): Promise<CheckoutStepResult> {
return updateShippingAddress(this.checkoutId, address);
}
async updateBillingAddress(address: Address): Promise<CheckoutStepResult> {
return updateBillingAddress(this.checkoutId, address);
}
async updateShippingMethod(shippingMethodId: string): Promise<CheckoutStepResult> {
return updateShippingMethod(this.checkoutId, shippingMethodId);
}
async updateMetadata(metadata: Record<string, string>): Promise<CheckoutStepResult> {
return updateCheckoutMetadata(this.checkoutId, metadata);
}
async complete(): Promise<CheckoutStepResult<{ id: string; number: string; languageCode: string }>> {
return completeCheckout(this.checkoutId);
}
/**
* Execute full checkout with validation
*/
async execute(input: Omit<CheckoutInput, "checkoutId">): Promise<CheckoutResult> {
const fullInput: CheckoutInput = {
...input,
checkoutId: this.checkoutId,
};
const validationError = validateCheckoutInput(fullInput);
if (validationError) {
return { success: false, error: validationError };
}
return executeCheckoutPipeline(fullInput);
}
}
// Factory function for creating checkout service
export function createCheckoutService(checkoutId: string): CheckoutService {
return new CheckoutService(checkoutId);
}

View File

@@ -75,6 +75,7 @@ interface SaleorCheckoutStore {
openCart: () => void;
closeCart: () => void;
clearError: () => void;
clearCheckout: () => void;
// Getters
getLineCount: () => number;
@@ -299,6 +300,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
closeCart: () => set({ isOpen: false }),
clearError: () => set({ error: null }),
setLanguageCode: (languageCode: string) => set({ languageCode }),
clearCheckout: () => set({ checkout: null, checkoutToken: null }),
getLineCount: () => {
const { checkout } = get();