diff --git a/docs/CHECKOUT_ARCHITECTURE_ANALYSIS.md b/docs/CHECKOUT_ARCHITECTURE_ANALYSIS.md new file mode 100644 index 0000000..6051c7f --- /dev/null +++ b/docs/CHECKOUT_ARCHITECTURE_ANALYSIS.md @@ -0,0 +1,317 @@ +# Checkout Architecture Analysis + +## What Broke: Root Cause Analysis + +### The Incident +Yesterday, checkout confirmation emails were working correctly in the customer's selected language. Today, they started arriving in English regardless of the customer's language preference. + +### Root Cause +**Implicit Dependency on Step Ordering** + +The checkout flow had a critical implicit requirement: the `languageCode` field MUST be set on the checkout object BEFORE calling `checkoutComplete`. This was discovered through trial and error, not through explicit architecture. + +### Why Small Changes Broke It + +The checkout flow was implemented as a **procedural monolith** in `page.tsx`: + +```typescript +// ❌ BEFORE: Monolithic function (440+ lines) +const handleSubmit = async () => { + // Step 1: Email + await updateEmail() + + // Step 2: Language ← This was added today + await updateLanguage() // <- Without this, emails are in wrong language! + + // Step 3: Addresses + await updateBillingAddress() + + // Step 4: Shipping + await updateShippingMethod() + + // Step 5: Metadata + await updateMetadata() + + // Step 6: Complete + await checkoutComplete() +} +``` + +**Problems with this approach:** + +1. **No explicit contracts**: Nothing says "language must be set before complete" +2. **Ordering is fragile**: Moving steps around breaks functionality +3. **No isolation**: Can't test individual steps +4. **Tight coupling**: UI, validation, API calls, and business logic all mixed +5. **No failure boundaries**: One failure stops everything, but unclear where + +## The Fix: Proper Abstraction + +### New Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ UI Layer (Page Component) │ +│ - Form handling │ +│ - Display logic │ +│ - Error display │ +└───────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Checkout Service Layer │ +│ - executeCheckoutPipeline() │ +│ - Enforces step ordering │ +│ - Validates inputs │ +│ - Handles failures │ +└───────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Individual Steps (Composable) │ +│ - updateCheckoutEmail() │ +│ - updateCheckoutLanguage() ← CRITICAL: Before complete! │ +│ - updateShippingAddress() │ +│ - updateBillingAddress() │ +│ - updateShippingMethod() │ +│ - updateCheckoutMetadata() │ +│ - completeCheckout() │ +└───────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Saleor API Client │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Key Improvements + +#### 1. **Explicit Pipeline** +```typescript +// ✅ AFTER: Explicit pipeline with enforced ordering +export async function executeCheckoutPipeline(input: CheckoutInput) { + // 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 }; + // ^^^ This MUST happen before complete - enforced by structure! + + // Step 3: Addresses + // ... + + // Step 7: Complete + return completeCheckout(checkoutId); +} +``` + +**Benefits:** +- Order is enforced by code structure, not comments +- Each step validates its result before continuing +- Clear failure points + +#### 2. **Composable Steps** +Each step is an independent, testable function: + +```typescript +// Can be tested in isolation +export async function updateCheckoutLanguage( + checkoutId: string, + languageCode: string +): Promise { + const { data } = await saleorClient.mutate({ + mutation: CHECKOUT_LANGUAGE_CODE_UPDATE, + variables: { checkoutId, languageCode }, + }); + + if (data?.checkoutLanguageCodeUpdate?.errors?.length) { + return { success: false, error: data.checkoutLanguageCodeUpdate.errors[0].message }; + } + + return { success: true }; +} +``` + +**Benefits:** +- Unit testable +- Can be reused in other flows +- Can be mocked for testing +- Clear input/output contracts + +#### 3. **Validation Separation** +```typescript +// Pure validation functions +export function validateAddress(address: Partial
): string | null { + if (!address.firstName?.trim()) return "First name is required"; + if (!address.phone?.trim() || address.phone.length < 8) return "Valid phone is required"; + return null; +} +``` + +**Benefits:** +- Validation is deterministic and testable +- No UI dependencies +- Can be reused + +#### 4. **Service Class for Complex Use Cases** +```typescript +// For cases that need step-by-step control +const checkoutService = createCheckoutService(checkoutId); +await checkoutService.updateEmail(email); +await checkoutService.updateLanguage(locale); // Explicitly called +// ... custom logic ... +await checkoutService.complete(); +``` + +## Comparison: Before vs After + +| Aspect | Before (Monolithic) | After (Service Layer) | +|--------|--------------------|----------------------| +| **Lines of code** | 440+ in one function | ~50 in UI, 300 in service | +| **Testability** | ❌ Can't unit test | ✅ Each step testable | +| **Step ordering** | ❌ Implicit/fragile | ✅ Enforced by structure | +| **Failure handling** | ❌ Try/catch spaghetti | ✅ Result-based, explicit | +| **Reusability** | ❌ Copy-paste only | ✅ Import and compose | +| **Type safety** | ⚠️ Inline types | ✅ Full TypeScript | +| **Documentation** | ❌ Comments only | ✅ Code is self-documenting | + +## Critical Business Rules Now Explicit + +```typescript +// These rules are now ENFORCED by code, not comments: + +// Rule 1: Language must be set before checkout completion +const languageResult = await updateCheckoutLanguage(checkoutId, languageCode); +if (!languageResult.success) { + return { success: false, error: languageResult.error }; // Pipeline stops! +} +// Only after success do we proceed to complete... + +// Rule 2: Any step failure stops the pipeline +const emailResult = await updateCheckoutEmail(checkoutId, email); +if (!emailResult.success) { + return { success: false, error: emailResult.error }; // Early return! +} + +// Rule 3: Validation happens before any API calls +const validationError = validateCheckoutInput(input); +if (validationError) { + return { success: false, error: validationError }; // Fail fast! +} +``` + +## Why This Won't Break Again + +### 1. **Enforced Ordering** +The pipeline function physically cannot complete checkout without first setting the language. It's not a comment—it's code structure. + +### 2. **Fail Fast** +Validation happens before any API calls. Invalid data never reaches Saleor. + +### 3. **Explicit Error Handling** +Each step returns a `CheckoutStepResult` with `success` boolean. No exceptions for flow control. + +### 4. **Composable Design** +If we need to add a new step (e.g., "apply coupon"), we insert it into the pipeline: +```typescript +const couponResult = await applyCoupon(checkoutId, couponCode); +if (!couponResult.success) return { success: false, error: couponResult.error }; +``` +The location in the pipeline shows its dependency order. + +### 5. **Type Safety** +TypeScript enforces that all required fields are present before the pipeline runs. + +## Migration Path + +### Phase 1: Keep Both (Current) +- Old code in `page.tsx` continues to work +- New service available for new features +- Gradual migration + +### Phase 2: Migrate UI +Replace the monolithic `handleSubmit` with service call: +```typescript +// In page.tsx +import { createCheckoutService } from '@/lib/services/checkoutService'; + +const handleSubmit = async () => { + const checkoutService = createCheckoutService(checkout.id); + + const result = await checkoutService.execute({ + email: shippingAddress.email, + shippingAddress: transformToServiceAddress(shippingAddress), + billingAddress: transformToServiceAddress(billingAddress), + shippingMethodId: selectedShippingMethod, + languageCode: locale, + metadata: { phone: shippingAddress.phone, userLanguage: locale }, + }); + + if (result.success) { + setOrderNumber(result.order!.number); + clearCheckout(); + } else { + setError(result.error); + } +}; +``` + +### Phase 3: Remove Old Code +Once confirmed working, remove the inline mutations from `page.tsx`. + +## Testing Strategy + +With the new architecture, we can test each component: + +```typescript +// Test individual steps +import { updateCheckoutLanguage, validateAddress } from './checkoutService'; + +describe('updateCheckoutLanguage', () => { + it('should fail if checkout does not exist', async () => { + const result = await updateCheckoutLanguage('invalid-id', 'EN'); + expect(result.success).toBe(false); + }); +}); + +describe('validateAddress', () => { + it('should require phone number', () => { + const error = validateAddress({ ...validAddress, phone: '' }); + expect(error).toContain('phone'); + }); +}); + +// Test full pipeline +import { executeCheckoutPipeline } from './checkoutService'; + +describe('executeCheckoutPipeline', () => { + it('should stop if language update fails', async () => { + // Mock language failure + jest.spyOn(checkoutService, 'updateCheckoutLanguage').mockResolvedValue({ + success: false, error: 'Language not supported' + }); + + const result = await executeCheckoutPipeline(validInput); + expect(result.success).toBe(false); + expect(result.error).toBe('Language not supported'); + }); +}); +``` + +## Conclusion + +The previous architecture was **accidentally fragile** because: +1. Business rules were implicit (language must be set before complete) +2. Step ordering was by convention, not enforcement +3. Everything was tightly coupled in one function +4. No clear boundaries between concerns + +The new architecture is **intentionally robust** because: +1. Business rules are enforced by code structure +2. Step ordering is physically enforced by the pipeline +3. Each component has a single, clear responsibility +4. Strong TypeScript contracts prevent misuse + +**Small changes will no longer break critical functionality** because the architecture makes dependencies explicit and enforces them at compile time and runtime. diff --git a/docs/COD-IMPLEMENTATION-PLAN.md b/docs/COD-IMPLEMENTATION-PLAN.md new file mode 100644 index 0000000..1bf42ea --- /dev/null +++ b/docs/COD-IMPLEMENTATION-PLAN.md @@ -0,0 +1,320 @@ +# Cash on Delivery (COD) Implementation Plan + +**Branch:** `feature/cash-on-delivery` +**Status:** In Development +**Created:** March 29, 2026 + +--- + +## 1. ARCHITECTURE DECISIONS + +### Payment Method Type: Simple Transaction +- Uses Saleor's native `Transaction` objects +- No Payment App required (COD is manual payment) +- Creates transaction with status `NOT_CHARGED` +- Staff marks as paid via Dashboard when cash collected + +### Why This Approach: +- ✅ Native Saleor data structures +- ✅ Appears in Dashboard automatically +- ✅ No metadata hacks +- ✅ Extensible to other simple payments (Bank Transfer) +- ✅ Compatible with Payment Apps later (Stripe, etc.) + +--- + +## 2. FILE STRUCTURE + +``` +src/ +├── lib/ +│ ├── config/ +│ │ └── paymentMethods.ts # Payment methods configuration +│ └── saleor/ +│ └── payments/ +│ ├── types.ts # Payment type definitions +│ ├── cod.ts # COD-specific logic +│ └── createTransaction.ts # Generic transaction creator +│ +├── components/ +│ └── payment/ +│ ├── PaymentMethodSelector.tsx # Payment method selection UI +│ ├── PaymentMethodCard.tsx # Individual payment card +│ └── CODInstructions.tsx # COD-specific instructions +│ +├── app/[locale]/checkout/ +│ ├── page.tsx # Updated checkout page +│ └── components/ +│ └── PaymentSection.tsx # Checkout payment section wrapper +│ +└── i18n/messages/ + ├── en.json # Payment translations + ├── sr.json # Payment translations + ├── de.json # Payment translations + └── fr.json # Payment translations +``` + +--- + +## 3. DATA MODELS + +### PaymentMethod Interface +```typescript +interface PaymentMethod { + id: string; + name: string; + description: string; + type: 'simple' | 'app'; + fee: number; + available: boolean; + availableInChannels: string[]; + icon?: string; +} +``` + +### COD Transaction Structure +```typescript +const codTransaction = { + name: "Cash on Delivery", + pspReference: `COD-${orderNumber}-${timestamp}`, + availableActions: ["CHARGE"], + amountAuthorized: { amount: 0, currency: "RSD" }, + amountCharged: { amount: 0, currency: "RSD" } +}; +``` + +--- + +## 4. IMPLEMENTATION PHASES + +### Phase 1: Configuration & Types (Files 1-3) +**Files:** +1. `lib/config/paymentMethods.ts` - Payment methods config +2. `lib/saleor/payments/types.ts` - Type definitions +3. `lib/saleor/payments/cod.ts` - COD transaction logic + +**Deliverables:** +- [ ] Payment methods configuration +- [ ] TypeScript interfaces +- [ ] COD transaction creation function + +### Phase 2: UI Components (Files 4-6) +**Files:** +4. `components/payment/PaymentMethodCard.tsx` +5. `components/payment/PaymentMethodSelector.tsx` +6. `components/payment/CODInstructions.tsx` + +**Deliverables:** +- [ ] Payment method selection UI +- [ ] COD instructions component +- [ ] Responsive design + +### Phase 3: Checkout Integration (Files 7-8) +**Files:** +7. `app/[locale]/checkout/components/PaymentSection.tsx` +8. `app/[locale]/checkout/page.tsx` (updated) + +**Deliverables:** +- [ ] Payment section in checkout +- [ ] Integration with checkout flow +- [ ] Transaction creation on complete + +### Phase 4: Translations (Files 9-12) +**Files:** +9-12. Update `i18n/messages/{en,sr,de,fr}.json` + +**Deliverables:** +- [ ] All translation keys +- [ ] Serbian, English, German, French + +### Phase 5: Testing +**Tasks:** +- [ ] Test COD flow end-to-end +- [ ] Verify transaction created in Saleor +- [ ] Test mobile responsiveness +- [ ] Test locale switching + +--- + +## 5. CHECKOUT FLOW + +``` +1. User adds items to cart + ↓ +2. User proceeds to checkout + ↓ +3. Checkout page loads with: + - Contact form (email, phone) + - Shipping address form + - Billing address form (same as shipping default) + - Shipping method selector + - PAYMENT METHOD SELECTOR (NEW) + └─ COD selected by default + - Order summary + - Complete Order button + ↓ +4. User fills all required fields + ↓ +5. User clicks "Complete Order" + ↓ +6. System: + a. Validates all fields + b. Creates order via checkoutComplete + c. Creates COD Transaction on order + d. Redirects to order confirmation + ↓ +7. Order Confirmation page shows: + - Order number + - Total amount + - Payment method: "Cash on Delivery" + - Instructions: "Please prepare cash for delivery" + ↓ +8. Staff sees order in Dashboard: + - Status: UNFULFILLED + - Payment Status: NOT_CHARGED + - Transaction: "Cash on Delivery (COD-123)" + ↓ +9. On delivery: + - Delivery person collects cash + - Staff marks order as FULFILLED in Dashboard + - (Optional: Create CHARGE_SUCCESS transaction event) +``` + +--- + +## 6. SALESOR DASHBOARD VIEW + +### Order Details: +``` +Order #1234 +├─ Status: UNFULFILLED +├─ Payment Status: NOT_CHARGED +├─ Transactions: +│ └─ Cash on Delivery (COD-1234-1743214567890) +│ ├─ Status: NOT_CHARGED +│ ├─ Amount: 3,200 RSD +│ └─ Available Actions: [CHARGE] +└─ Actions: [Fulfill] [Cancel] +``` + +### When Cash Collected: +``` +Staff clicks [Fulfill] + ↓ +Order Status: FULFILLED +Payment Status: (still NOT_CHARGED, but order is complete) +``` + +--- + +## 7. TRANSLATION KEYS + +### English (en.json): +```json +{ + "Payment": { + "title": "Payment Method", + "cod": { + "name": "Cash on Delivery", + "description": "Pay when you receive your order", + "instructions": { + "title": "Payment Instructions", + "prepareCash": "Please prepare the exact amount in cash", + "inspectOrder": "You can inspect your order before paying", + "noFee": "No additional fee for cash on delivery" + } + }, + "card": { + "name": "Credit Card", + "description": "Secure online payment", + "comingSoon": "Coming soon" + }, + "selectMethod": "Select payment method", + "securePayment": "Secure payment processing" + } +} +``` + +### Serbian (sr.json): +```json +{ + "Payment": { + "title": "Način Plaćanja", + "cod": { + "name": "Plaćanje Pouzećem", + "description": "Platite kada primite porudžbinu", + "instructions": { + "title": "Uputstva za Plaćanje", + "prepareCash": "Pripremite tačan iznos u gotovini", + "inspectOrder": "Možete pregledati porudžbinu pre plaćanja", + "noFee": "Bez dodatne naknade za plaćanje pouzećem" + } + } + } +} +``` + +--- + +## 8. TESTING CHECKLIST + +### Functional Tests: +- [ ] COD radio button selected by default +- [ ] Payment section visible in checkout +- [ ] Order completes with COD selected +- [ ] Transaction created with correct details +- [ ] Transaction visible in Saleor Dashboard +- [ ] Order confirmation shows COD +- [ ] Translations work in all locales + +### Edge Cases: +- [ ] Checkout validation fails - payment method preserved +- [ ] Network error during transaction creation +- [ ] User switches payment methods (when multiple available) +- [ ] Mobile viewport - payment section responsive + +### Integration Tests: +- [ ] End-to-end COD flow +- [ ] Order appears in Dashboard +- [ ] Staff can fulfill COD order +- [ ] Multiple payment methods display correctly + +--- + +## 9. FUTURE ENHANCEMENTS + +### Phase 2 (Post-MVP): +- [ ] Add Bank Transfer payment method +- [ ] Payment method icons +- [ ] Save payment preference for logged-in users + +### Phase 3 (Advanced): +- [ ] Bitcoin (manual) payment method +- [ ] Bitcoin (automated) via custom handler +- [ ] Payment Apps integration (Stripe, etc.) + +--- + +## 10. NOTES + +### Why No Metadata: +- Saleor has native Transaction objects +- Transactions are typed and validated +- Appear in Dashboard automatically +- Support proper lifecycle (NOT_CHARGED → CHARGED) + +### Why Simple Type (Not App): +- COD doesn't need async processing +- No external API to integrate +- No PCI compliance requirements +- Manual verification by staff + +### Compatibility: +- Current architecture supports Payment Apps later +- Can add Stripe/PayPal as `type: 'app'` without breaking COD +- Bitcoin can be added as `type: 'async'` when ready + +--- + +**Last Updated:** March 29, 2026 +**Next Review:** After Phase 1 completion \ No newline at end of file diff --git a/hash.py b/hash.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/[locale]/checkout/components/PaymentSection.tsx b/src/app/[locale]/checkout/components/PaymentSection.tsx new file mode 100644 index 0000000..8640a9a --- /dev/null +++ b/src/app/[locale]/checkout/components/PaymentSection.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { PaymentMethodSelector, CODInstructions } from "@/components/payment"; +import { getPaymentMethodsForChannel } from "@/lib/config/paymentMethods"; +import type { PaymentMethod } from "@/lib/saleor/payments/types"; +import { useTranslations } from "next-intl"; + +interface PaymentSectionProps { + selectedMethodId: string; + onSelectMethod: (methodId: string) => void; + locale: string; + channel?: string; + disabled?: boolean; +} + +export function PaymentSection({ + selectedMethodId, + onSelectMethod, + locale, + channel = "default-channel", + disabled = false, +}: PaymentSectionProps) { + const t = useTranslations("Payment"); + + // Get available payment methods for this channel + const paymentMethods: PaymentMethod[] = getPaymentMethodsForChannel(channel); + + // Get the selected method details + const selectedMethod = paymentMethods.find((m) => m.id === selectedMethodId); + + return ( +
+ + + {/* COD instructions can be shown here if needed */} + {selectedMethod?.id === "cod" && ( + + )} +
+ ); +} diff --git a/src/app/[locale]/checkout/page.tsx b/src/app/[locale]/checkout/page.tsx index efce7f9..cfcac3d 100644 --- a/src/app/[locale]/checkout/page.tsx +++ b/src/app/[locale]/checkout/page.tsx @@ -13,14 +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_CONFIRM, } 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?: { @@ -29,44 +28,6 @@ interface ShippingAddressUpdateResponse { }; } -interface BillingAddressUpdateResponse { - checkoutBillingAddressUpdate?: { - checkout?: Checkout; - errors?: Array<{ message: string }>; - }; -} - -interface CheckoutCompleteResponse { - checkoutComplete?: { - order?: { 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; } @@ -96,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(null); @@ -104,6 +65,7 @@ export default function CheckoutPage() { const [orderNumber, setOrderNumber] = useState(null); const [sameAsShipping, setSameAsShipping] = useState(true); + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(DEFAULT_PAYMENT_METHOD); const [shippingAddress, setShippingAddress] = useState({ firstName: "", lastName: "", @@ -277,131 +239,115 @@ export default function CheckoutPage() { return; } + if (!selectedPaymentMethod) { + setError(t("errorSelectPayment")); + return; + } + setIsLoading(true); setError(null); try { - console.log("Completing order..."); + console.log("Completing order via CheckoutService..."); - console.log("Step 1: Updating email..."); - const emailResult = await saleorClient.mutate({ - mutation: CHECKOUT_EMAIL_UPDATE, + // Create checkout service instance + const checkoutService = createCheckoutService(checkout.id); + + // 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; + } + throw new Error(result.error || t("errorCreatingOrder")); + } + + // Success! + setOrderNumber(result.order.number); + setOrderComplete(true); + + // Auto-confirm the order + try { + console.log("Auto-confirming order:", result.order.id); + await saleorClient.mutate({ + mutation: ORDER_CONFIRM, variables: { - checkoutId: checkout.id, - email: shippingAddress.email, + orderId: result.order.id, }, }); + console.log("Order confirmed successfully"); + } catch (confirmError) { + console.error("Failed to auto-confirm order:", confirmError); + // Don't fail the checkout if confirmation 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, + }); - 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}`); - } - console.log("Step 1: Email updated successfully"); + console.log("Order completed successfully:", result.order.number); - console.log("Step 2: Updating billing address..."); - const billingResult = await saleorClient.mutate({ - 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 2: Billing address updated successfully"); - - console.log("Step 3: Setting shipping method..."); - const shippingMethodResult = await saleorClient.mutate({ - 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 3: Shipping method set successfully"); - - console.log("Step 4: Saving metadata..."); - const metadataResult = await saleorClient.mutate({ - 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 4: Phone number saved successfully"); - } - - console.log("Step 5: Completing checkout..."); - const completeResult = await saleorClient.mutate({ - 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, - }); - - // Identify the user - identifyUser({ - profileId: shippingAddress.email, - email: shippingAddress.email, - firstName: shippingAddress.firstName, - lastName: shippingAddress.lastName, - }); - } else { - throw new Error(t("errorCreatingOrder")); - } } catch (err: unknown) { console.error("Checkout error:", err); @@ -660,6 +606,23 @@ export default function CheckoutPage() { )} + {/* Payment Method Section */} + + + {/* Money Back Guarantee Trust Badge */} +
+ + + + {t("moneyBackGuarantee")} +
+