@@ -49,7 +46,11 @@ export default async function CartPage() {
)}
@@ -65,19 +66,23 @@ export default async function CartPage() {
))}
-
-
-
- جمع کل سبد ({cart.groups.length.toLocaleString("fa-IR")} فروشگاه،{" "}
- {totalItemLines.toLocaleString("fa-IR")} کالا)
+
+
+
+
+ جمع کل سبد ({cart.groups.length.toLocaleString("fa-IR")} فروشگاه،{" "}
+ {totalItemLines.toLocaleString("fa-IR")} کالا)
+
+
+
+ هر فروشگاه جداگانه پرداخت و ارسال میشود
+
+
+
+
+ {cart.grand_total.toLocaleString("fa-IR")} تومان
-
- هر فروشگاه جداگانه پرداخت و ارسال میشود
-
-
- {cart.grand_total.toLocaleString("fa-IR")} تومان
-
);
diff --git a/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx b/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx
index 9411ca7..588a082 100644
--- a/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx
+++ b/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx
@@ -4,10 +4,16 @@ import { ArrowRight } from "lucide-react";
import { getCart } from "@/lib/api/cart";
import { getAddresses } from "@/lib/api/addresses";
import { requireSession } from "@/lib/auth/session";
+import { CheckoutAddress } from "@/components/customer/checkout-address";
import { CheckoutPayment } from "@/components/customer/checkout-payment";
-export default async function ConfirmPaymentPage({ params }: PageProps<"/checkout/[store]/confirm">) {
+export default async function ConfirmPaymentPage({
+ params,
+ searchParams,
+}: PageProps<"/checkout/[store]/confirm">) {
const { store } = await params;
+ const { fulfillment } = await searchParams;
+ const isDelivery = fulfillment === "delivery";
await requireSession(`/checkout/${store}/confirm`);
const [cart, addresses] = await Promise.all([
@@ -19,6 +25,8 @@ export default async function ConfirmPaymentPage({ params }: PageProps<"/checkou
const group = cart.groups.find((g) => g.store.uuid === store);
if (!group) notFound();
+ const address = addresses.find((a) => a.is_default) ?? addresses[0] ?? null;
+
return (
{/* Same header pattern as /cart and /checkout/[store]. */}
@@ -29,7 +37,11 @@ export default async function ConfirmPaymentPage({ params }: PageProps<"/checkou
تأیید و پرداخت
-
+ {isDelivery && (
+
+ )}
+
+
);
}
diff --git a/src/app/(customer)/(shop)/checkout/page.tsx b/src/app/(customer)/(shop)/checkout/page.tsx
deleted file mode 100644
index 2182a1b..0000000
--- a/src/app/(customer)/(shop)/checkout/page.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { redirect } from "next/navigation";
-import { getCart } from "@/lib/api/cart";
-import { requireSession } from "@/lib/auth/session";
-import { getAddresses } from "@/lib/api/addresses";
-import { CheckoutForm } from "@/components/customer/checkout-form";
-
-export default async function CheckoutPage() {
- await requireSession("/checkout");
- const [cart, addresses] = await Promise.all([getCart(), getAddresses()]);
-
- if (cart.groups.length === 0) redirect("/cart");
-
- return (
-
- تکمیل سفارش
-
-
- );
-}
diff --git a/src/components/customer/address-details-form.tsx b/src/components/customer/address-details-form.tsx
new file mode 100644
index 0000000..c745c81
--- /dev/null
+++ b/src/components/customer/address-details-form.tsx
@@ -0,0 +1,240 @@
+"use client";
+
+import { useMemo, useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { createAddress, type AddressInput } from "@/lib/api/addresses";
+import type { AddressLabel, City, Neighborhood } from "@/types/api";
+
+const LABEL_PRESETS: { value: AddressLabel; text: string }[] = [
+ { value: "other", text: "سایر" },
+ { value: "work", text: "محل کار" },
+ { value: "home", text: "خانه جدید" },
+];
+
+export function AddressDetailsForm({
+ fullAddress,
+ latitude,
+ longitude,
+ cityUuid,
+ neighborhoodUuid,
+ cities,
+ neighborhoods,
+ next,
+}: {
+ fullAddress: string;
+ latitude: number;
+ longitude: number;
+ /** Inferred from the customer's current location cookie, if they'd set one manually. */
+ cityUuid: string | null;
+ neighborhoodUuid: string | null;
+ cities: City[];
+ neighborhoods: Neighborhood[];
+ /** Set when this add-address detour started from checkout — see handleSubmit. */
+ next: string | null;
+}) {
+ const router = useRouter();
+ const [address, setAddress] = useState(fullAddress);
+ const [label, setLabel] = useState
("home");
+ const [title, setTitle] = useState("خانه جدید");
+ const [plaque, setPlaque] = useState("");
+ const [floor, setFloor] = useState("");
+ const [unit, setUnit] = useState("");
+ const [recipientName, setRecipientName] = useState("");
+ const [recipientPhone, setRecipientPhone] = useState("");
+ const [manualCityUuid, setManualCityUuid] = useState(cityUuid ?? "");
+ const [manualNeighborhoodUuid, setManualNeighborhoodUuid] = useState(neighborhoodUuid ?? "");
+ const [error, setError] = useState(null);
+ const [isPending, startTransition] = useTransition();
+
+ // city_uuid is a hard requirement on the backend (OpenAPI: Address.city_uuid is
+ // required for writes) but isn't part of this screen's design — it's normally
+ // inferred from the customer's current location cookie. Only show a picker for
+ // it when that inference failed (e.g. they're in "geo" mode, or never set one).
+ const needsCityPicker = !cityUuid;
+ const filteredNeighborhoods = useMemo(
+ () => neighborhoods.filter((n) => n.city.uuid === manualCityUuid),
+ [neighborhoods, manualCityUuid],
+ );
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+
+ const finalCityUuid = cityUuid ?? manualCityUuid;
+ if (!finalCityUuid) {
+ setError("شهر مشخص نیست — لطفاً محله خود را از صفحه اصلی انتخاب کنید یا شهر را زیر مشخص کنید.");
+ return;
+ }
+
+ startTransition(async () => {
+ const input: AddressInput = {
+ label,
+ title,
+ full_address: address,
+ plaque,
+ floor,
+ unit,
+ recipient_name: recipientName,
+ recipient_phone: recipientPhone,
+ city_uuid: finalCityUuid,
+ neighborhood_uuid: (neighborhoodUuid ?? manualNeighborhoodUuid) || undefined,
+ latitude,
+ longitude,
+ // Arriving here from checkout means they had no address at all yet — make
+ // this one the default immediately so it's actually used for the order,
+ // rather than sending them on to /addresses to select it a second time.
+ // A plain profile-driven "add another address" leaves the existing default alone.
+ is_default: next !== null,
+ };
+ const result = await createAddress(input);
+ if (result.ok) {
+ router.push(next ?? "/addresses");
+ } else {
+ setError(result.message);
+ }
+ });
+ }
+
+ function selectPreset(preset: (typeof LABEL_PRESETS)[number]) {
+ setLabel(preset.value);
+ setTitle(preset.text);
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/customer/address-list.tsx b/src/components/customer/address-list.tsx
deleted file mode 100644
index c80af60..0000000
--- a/src/components/customer/address-list.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-"use client";
-
-import { useMemo, useState, useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { createAddress, deleteAddress, type AddressInput } from "@/lib/api/addresses";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import type { Address, AddressLabel, City, Neighborhood } from "@/types/api";
-
-const LABEL_OPTIONS: { value: AddressLabel; label: string }[] = [
- { value: "home", label: "خانه" },
- { value: "work", label: "محل کار" },
- { value: "other", label: "سایر" },
-];
-
-const emptyForm = {
- label: "home" as AddressLabel,
- title: "",
- full_address: "",
- plaque: "",
- floor: "",
- unit: "",
- recipient_name: "",
- recipient_phone: "",
- city_uuid: "",
- neighborhood_uuid: "",
- is_default: false,
-};
-
-export function AddressList({
- addresses,
- cities,
- neighborhoods,
-}: {
- addresses: Address[];
- cities: City[];
- neighborhoods: Neighborhood[];
-}) {
- const router = useRouter();
- const [isAdding, setIsAdding] = useState(false);
- const [form, setForm] = useState(emptyForm);
- const [error, setError] = useState(null);
- const [isPending, startTransition] = useTransition();
-
- const filteredNeighborhoods = useMemo(
- () => neighborhoods.filter((n) => n.city.uuid === form.city_uuid),
- [neighborhoods, form.city_uuid],
- );
-
- function handleDelete(uuid: string) {
- startTransition(async () => {
- await deleteAddress(uuid);
- router.refresh();
- });
- }
-
- function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- setError(null);
- startTransition(async () => {
- const input: AddressInput = {
- ...form,
- neighborhood_uuid: form.neighborhood_uuid || undefined,
- latitude: 0,
- longitude: 0,
- };
- const result = await createAddress(input);
- if (result.ok) {
- setForm(emptyForm);
- setIsAdding(false);
- router.refresh();
- } else {
- setError(result.message);
- }
- });
- }
-
- return (
-
-
- {addresses.map((address) => (
-
-
-
- {LABEL_OPTIONS.find((o) => o.value === address.label)?.label} {address.is_default && "· پیشفرض"}
-
- {address.full_address}
-
- {address.recipient_name} — {address.recipient_phone}
-
-
-
handleDelete(address.uuid)}>
- حذف
-
-
- ))}
- {addresses.length === 0 && !isAdding &&
آدرسی ثبت نشده است.
}
-
-
- {isAdding ? (
-
- ) : (
-
setIsAdding(true)}>
- + افزودن آدرس جدید
-
- )}
-
- );
-}
diff --git a/src/components/customer/address-selector.tsx b/src/components/customer/address-selector.tsx
new file mode 100644
index 0000000..b718482
--- /dev/null
+++ b/src/components/customer/address-selector.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { updateAddress } from "@/lib/api/addresses";
+import type { Address, AddressLabel } from "@/types/api";
+
+const ADDRESS_LABEL_FA: Record = {
+ home: "خانه",
+ work: "محل کار",
+ other: "سایر",
+};
+
+/**
+ * Doubles as both a plain address-management screen (reached from the profile menu)
+ * and a picker (reached from checkout's "تغییر آدرس" with a `next` return path) —
+ * the radio always reflects/sets which address is the default, "تأیید" just persists
+ * that choice and, if there's somewhere to return to, goes back there.
+ */
+export function AddressSelector({ addresses, next }: { addresses: Address[]; next: string | null }) {
+ const router = useRouter();
+ const [selectedUuid, setSelectedUuid] = useState(
+ () => addresses.find((a) => a.is_default)?.uuid ?? addresses[0]?.uuid ?? null,
+ );
+ const [error, setError] = useState(null);
+ const [isPending, startTransition] = useTransition();
+
+ function handleConfirm() {
+ setError(null);
+ const currentDefaultUuid = addresses.find((a) => a.is_default)?.uuid;
+ if (!selectedUuid || selectedUuid === currentDefaultUuid) {
+ if (next) router.push(next);
+ return;
+ }
+ startTransition(async () => {
+ const result = await updateAddress(selectedUuid, { is_default: true });
+ if (result.ok) {
+ if (next) router.push(next);
+ else router.refresh();
+ } else {
+ setError(result.message);
+ }
+ });
+ }
+
+ return (
+
+
+ {addresses.map((address) => {
+ const isSelected = address.uuid === selectedUuid;
+ return (
+
setSelectedUuid(address.uuid)}
+ className={`flex flex-col gap-1 rounded-2xl border p-4 text-right ${
+ isSelected ? "border-primary-action" : "border-gray-300"
+ }`}
+ >
+ {/* DOM order intentionally mirrors the screenshot's left→right layout —
+ see the inherited dir="rtl" note on the /cart page. */}
+
+
+ {address.title || ADDRESS_LABEL_FA[address.label]}
+
+
+ {isSelected && }
+
+
+ {address.full_address}
+
+ );
+ })}
+
+ {addresses.length === 0 && (
+
هنوز آدرسی ثبت نکردهاید.
+ )}
+
+
router.push(next ? `/addresses/map?next=${encodeURIComponent(next)}` : "/addresses/map")}
+ className="rounded-2xl border border-primary-action py-4 text-[14px] font-bold text-primary-action"
+ >
+ افزودن آدرس جدید
+
+
+
+ {error &&
{error}
}
+
+ {addresses.length > 0 && (
+
+ {isPending ? "در حال ذخیره..." : "تأیید"}
+
+ )}
+
+ );
+}
diff --git a/src/components/customer/checkout-address.tsx b/src/components/customer/checkout-address.tsx
new file mode 100644
index 0000000..5c64ed9
--- /dev/null
+++ b/src/components/customer/checkout-address.tsx
@@ -0,0 +1,73 @@
+import Link from "next/link";
+import { MapPin } from "lucide-react";
+import type { Address, AddressLabel } from "@/types/api";
+
+const ADDRESS_LABEL_FA: Record = {
+ home: "خانه",
+ work: "محل کار",
+ other: "سایر",
+};
+
+/**
+ * Both actions carry `returnTo` (the checkout confirm page's own URL) so the whole
+ * address-management chain — map → details form → address list — comes back here
+ * instead of stranding the customer on /addresses mid-checkout.
+ */
+export function CheckoutAddress({
+ address,
+ returnTo,
+}: {
+ address: Address | null;
+ returnTo: string;
+}) {
+ const next = encodeURIComponent(returnTo);
+ return (
+
+
+ آدرس تحویل
+
+
+ {address ? (
+
+ {/* DOM order intentionally mirrors the screenshot's left→right layout — see the
+ inherited dir="rtl" note on the /cart page. */}
+
+
+
+
+ {address.title || ADDRESS_LABEL_FA[address.label]}
+
+
+
+ {address.full_address}
+
+
+
+ تغییر آدرس
+
+
+ ) : (
+
+
+
+
+
+ هنوز آدرسی ثبت نکردهاید. برای ادامه سفارش، لطفاً یک آدرس اضافه
+ کنید.
+
+
+
+ افزودن آدرس
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/customer/checkout-form.tsx b/src/components/customer/checkout-form.tsx
deleted file mode 100644
index a8af62d..0000000
--- a/src/components/customer/checkout-form.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { submitCheckout } from "@/lib/api/checkout";
-import { Button } from "@/components/ui/button";
-import type { Address, Cart, DeliveryType, PaymentMethod } from "@/types/api";
-
-const PAYMENT_LABELS_FA: Record = {
- wallet: "کیف پول وینوفای",
- online: "درگاه بانکی",
- cash_on_delivery: "پرداخت در محل",
-};
-
-export function CheckoutForm({ cart, addresses }: { cart: Cart; addresses: Address[] }) {
- const router = useRouter();
- const [addressUuid, setAddressUuid] = useState(addresses.find((a) => a.is_default)?.uuid ?? addresses[0]?.uuid ?? "");
- const [deliveryType, setDeliveryType] = useState("express");
- const [scheduledAt, setScheduledAt] = useState("");
- const [paymentMethod, setPaymentMethod] = useState("cash_on_delivery");
- const [notes, setNotes] = useState("");
- const [error, setError] = useState(null);
- const [isPending, startTransition] = useTransition();
-
- function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- setError(null);
-
- if (!addressUuid) {
- setError("لطفاً یک آدرس انتخاب کنید.");
- return;
- }
- if (deliveryType === "scheduled" && !scheduledAt) {
- setError("لطفاً زمان ارسال را مشخص کنید.");
- return;
- }
-
- startTransition(async () => {
- const result = await submitCheckout({
- address_uuid: addressUuid,
- delivery_type: deliveryType,
- scheduled_at: deliveryType === "scheduled" ? new Date(scheduledAt).toISOString() : null,
- payment_method: paymentMethod,
- notes,
- });
- if (result.ok) {
- router.push(`/order-groups/${result.data.uuid}`);
- } else {
- setError(result.message);
- }
- });
- }
-
- if (addresses.length === 0) {
- return (
-
-
برای ثبت سفارش ابتدا یک آدرس اضافه کنید.
-
-
افزودن آدرس
-
-
- );
- }
-
- return (
-
- );
-}
diff --git a/src/components/customer/checkout-fulfillment.tsx b/src/components/customer/checkout-fulfillment.tsx
index a3e638a..202b38e 100644
--- a/src/components/customer/checkout-fulfillment.tsx
+++ b/src/components/customer/checkout-fulfillment.tsx
@@ -94,9 +94,12 @@ export function CheckoutFulfillment({ group }: { group: CartGroup }) {
- {/* Pickup has its own confirm+pay screen (no address needed); delivery still
- goes to the general /checkout form until that flow's design is provided. */}
-
+ {/* Same confirm+pay screen either way — it shows the address section only for
+ delivery, and folds the delivery fee into the total only for delivery. */}
+
("wallet");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
- // Pickup flow — no delivery fee, matches the fulfillment screen's "دریافت حضوری" total.
- const payable = group.items_subtotal;
+ // Pickup — no delivery fee, matches the fulfillment screen's "دریافت حضوری" total.
+ const payable = group.items_subtotal + (isDelivery ? group.delivery_fee : 0);
function handlePay() {
setError(null);
@@ -29,14 +37,16 @@ export function CheckoutPayment({ group, addresses }: { group: CartGroup; addres
return;
}
startTransition(async () => {
- // submitCheckout processes the whole cart at once (splits into per-store orders
- // server-side), not just this store — see PageProps note on the confirm page.
const result = await submitCheckout({
address_uuid: addressUuid,
delivery_type: "express",
scheduled_at: null,
payment_method: method,
notes: "",
+ // Scopes this checkout to just this store's cart group — without this,
+ // the backend checked out the entire cart regardless of which store's
+ // "پرداخت" button was pressed.
+ store_uuids: [group.store.uuid],
});
if (result.ok) {
router.push(`/order-groups/${result.data.uuid}`);
diff --git a/src/components/location/address-map-leaflet.tsx b/src/components/location/address-map-leaflet.tsx
new file mode 100644
index 0000000..d36e365
--- /dev/null
+++ b/src/components/location/address-map-leaflet.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import "leaflet/dist/leaflet.css";
+import { useState } from "react";
+import { MapContainer, TileLayer, ZoomControl, useMap, useMapEvents } from "react-leaflet";
+import { LocateFixed } from "lucide-react";
+
+const ADDWIN_TILE_URL = "https://maps.addwin.ir/styles/basic/{z}/{x}/{y}.png";
+
+/**
+ * Reports the map's center whenever panning/zooming settles — this drives the
+ * "pin fixed at screen-center, map moves underneath" UX (see the CSS pin in
+ * AddressMapLeaflet below), not a draggable react-leaflet .
+ */
+function CenterTracker({ onMove }: { onMove: (lat: number, lng: number) => void }) {
+ useMapEvents({
+ moveend: (e) => {
+ const c = e.target.getCenter();
+ onMove(c.lat, c.lng);
+ },
+ });
+ return null;
+}
+
+/** Recenters the map on the browser's reported GPS position — same navigator.geolocation
+ * pattern as the location-onboarding flow (permission/error screens). */
+function RecenterButton() {
+ const map = useMap();
+ const [locating, setLocating] = useState(false);
+
+ function handleClick() {
+ setLocating(true);
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ map.flyTo([position.coords.latitude, position.coords.longitude], 16);
+ setLocating(false);
+ },
+ () => setLocating(false),
+ { timeout: 10000 },
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export function AddressMapLeaflet({
+ initialCenter,
+ onCenterChange,
+}: {
+ initialCenter: [number, number];
+ onCenterChange: (lat: number, lng: number) => void;
+}) {
+ return (
+
+
+
+
+
+
+
+
+ {/* Fixed screen-center pin — its tip (not its center) marks the selected point,
+ so it's offset up by its own height rather than centered on both axes. */}
+
+
+ );
+}
diff --git a/src/components/location/address-map-picker.tsx b/src/components/location/address-map-picker.tsx
new file mode 100644
index 0000000..f32f0bc
--- /dev/null
+++ b/src/components/location/address-map-picker.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { useState } from "react";
+import dynamic from "next/dynamic";
+import { useRouter, useSearchParams } from "next/navigation";
+
+// Leaflet touches `window` at import time — this component can only ever run in
+// the browser, so it's dynamically imported with ssr:false from this client
+// component (Server Components/pages can't pass ssr:false themselves in the
+// App Router, hence this wrapper layer).
+const AddressMapLeaflet = dynamic(
+ () => import("./address-map-leaflet").then((m) => m.AddressMapLeaflet),
+ {
+ ssr: false,
+ loading: () =>
,
+ },
+);
+
+// Central Tehran — used only when there's no prior location to center on.
+const DEFAULT_CENTER: [number, number] = [35.7219, 51.339];
+
+export function AddressMapPicker({ initialCenter }: { initialCenter: [number, number] | null }) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const next = searchParams.get("next");
+ const [center, setCenter] = useState(initialCenter ?? DEFAULT_CENTER);
+ const [address, setAddress] = useState("");
+
+ function handleConfirm() {
+ const params = new URLSearchParams({
+ lat: String(center[0]),
+ lng: String(center[1]),
+ address,
+ });
+ if (next) params.set("next", next);
+ router.push(`/addresses/new?${params.toString()}`);
+ }
+
+ return (
+
+
+
setCenter([lat, lng])} />
+
+
+
+
+ موقعیت انتخابی
+ {/* No reverse-geocoding endpoint is documented for Addwin or Winofy (checked
+ the OpenAPI spec — Address only ever takes latitude/longitude and a
+ separate free-text full_address, never derives one from the other), so
+ this is a real editable field styled to match the design, not an
+ auto-filled label. */}
+ setAddress(e.target.value)}
+ placeholder="آدرس را وارد کنید — مثلاً خیابان، کوچه، پلاک"
+ className="border-0 bg-transparent p-0 text-[15px] font-semibold leading-normal text-foreground outline-none placeholder:font-normal placeholder:text-gray-400"
+ />
+
+
+
+ تأیید
+
+
+
+ );
+}
diff --git a/src/lib/api/checkout.ts b/src/lib/api/checkout.ts
index dc10ce5..1697d6d 100644
--- a/src/lib/api/checkout.ts
+++ b/src/lib/api/checkout.ts
@@ -1,5 +1,6 @@
"use server";
+import { revalidatePath } from "next/cache";
import { winofyFetch } from "./winofy";
import { callApi, type ActionResult } from "./result";
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
@@ -10,8 +11,21 @@ export interface CheckoutInput {
scheduled_at: string | null;
payment_method: PaymentMethod;
notes: string;
+ /**
+ * Which store-cart-groups to check out, by store uuid. Required (not optional) —
+ * every caller must be explicit about scope now; there's no more implicit
+ * "everything in the cart" default. This is what fixes the bug where paying for
+ * one store's order submitted every store's order: previously nothing on this
+ * request identified which store(s) were intended, so the backend checked out
+ * the whole cart every time.
+ */
+ store_uuids: string[];
}
export async function submitCheckout(input: CheckoutInput): Promise> {
- return callApi(() => winofyFetch("/v1/checkout/", { method: "POST", body: input }));
+ const result = await callApi(() => winofyFetch("/v1/checkout/", { method: "POST", body: input }));
+ // Only the scoped store(s)' items are removed server-side now — the cart page
+ // (and the sticky-cart-bar badge) needs a fresh fetch to stop showing them.
+ if (result.ok) revalidatePath("/cart");
+ return result;
}
diff --git a/src/lib/api/gooyal-accounts.ts b/src/lib/api/gooyal-accounts.ts
index 92a0e16..c1f1f5f 100644
--- a/src/lib/api/gooyal-accounts.ts
+++ b/src/lib/api/gooyal-accounts.ts
@@ -1,7 +1,7 @@
import "server-only";
import { getValidSession } from "@/lib/auth/session";
import { ApiError, type WinofyErrorDetails } from "./errors";
-import { logWinofyRequest, logWinofyResponse } from "./dev-logger";
+import { logApiRequest, logApiResponse } from "./dev-logger";
const BASE_URL = process.env.GOOYAL_ACCOUNTS_BASE_URL!;
@@ -35,7 +35,7 @@ export async function gooyalFetch(path: string, options: GooyalFetchOptions =
}
}
- logWinofyRequest?.(method, url.toString(), body);
+ logApiRequest("gooyal", method, url.toString(), body);
const res = await fetch(url.toString(), {
...rest,
@@ -45,12 +45,12 @@ export async function gooyalFetch(path: string, options: GooyalFetchOptions =
});
if (res.status === 204) {
- logWinofyResponse?.(method, url.toString(), res.status, null);
+ logApiResponse("gooyal", method, url.toString(), res.status, null);
return undefined as T;
}
const data = await res.json().catch(() => null);
- logWinofyResponse?.(method, url.toString(), res.status, data);
+ logApiResponse("gooyal", method, url.toString(), res.status, data);
if (!res.ok) {
const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null;