@raxonltd/raxon-core 1.1.7 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/component/general.image.tsx +86 -0
- package/core/context/cart.context.tsx +446 -0
- package/core/context/security.context.tsx +151 -0
- package/core/feature/address/api/places.api.ts +76 -0
- package/core/feature/address/form/address-search-input.tsx +125 -0
- package/core/feature/address/hook/use.addres.tsx +63 -0
- package/core/feature/address/hook/use.address-autocomplete.ts +116 -0
- package/core/feature/address/util/address.types.ts +38 -0
- package/core/feature/address/util/parse-google-place.ts +66 -0
- package/core/feature/analytic-event/analytic.event.api.ts +27 -0
- package/core/feature/analytic-event/analytic.event.context.tsx +180 -0
- package/core/feature/analytic-event/analytic.event.util.ts +42 -0
- package/core/feature/analytic-event/use.analytic.auto.tsx +114 -0
- package/core/feature/article/hook/use.article.tsx +33 -0
- package/core/feature/attribute/hook/use.attribute.tsx +24 -0
- package/core/feature/auth/hook/use.auth.tsx +141 -0
- package/core/feature/auth/modal/modal.auth.tsx +80 -0
- package/core/feature/auth/view/view.login.tsx +199 -0
- package/core/feature/auth/view/view.register.tsx +333 -0
- package/core/feature/bank-account/hook/use.bank.account.tsx +47 -0
- package/core/feature/brand/hook/use.brand.tsx +24 -0
- package/core/feature/cart/component/cart.order.summary.tsx +89 -0
- package/core/feature/cart/component/cart.promo.code.section.tsx +208 -0
- package/core/feature/cart/hook/use.cart.tsx +267 -0
- package/core/feature/cart/util/basket-pay.response.ts +67 -0
- package/core/feature/cart/util/cart-optimistic.ts +425 -0
- package/core/feature/cart/util/garanti-payment.ts +27 -0
- package/core/feature/collection/hook/use.collection.tsx +32 -0
- package/core/feature/delivery-method/hook/use.delivery.method.tsx +40 -0
- package/core/feature/delivery-method/util/checkout.delivery.method.ts +11 -0
- package/core/feature/faq/hook/use.faq.tsx +23 -0
- package/core/feature/favorite/hook/use.favorite.tsx +48 -0
- package/core/feature/form-submit/form/form.contact.tsx +118 -0
- package/core/feature/form-submit/hook/use.form.submit.tsx +16 -0
- package/core/feature/invoice/hook/use.invoice.tsx +51 -0
- package/core/feature/newsletter/hook/use.newsletter.tsx +124 -0
- package/core/feature/newsletter/modal/modal.newsletter.product.tsx +163 -0
- package/core/feature/order/hook/use.order.tsx +31 -0
- package/core/feature/payment-method/checkout.payment.options.ts +117 -0
- package/core/feature/payment-method/hook/use.payment.method.tsx +44 -0
- package/core/feature/product/hook/use.product.tsx +122 -0
- package/core/feature/profile/hook/use.profile.tsx +126 -0
- package/core/feature/promo-code/hook/use.promo.code.tsx +27 -0
- package/core/interface/basket.interface.ts +360 -0
- package/core/interface/bootstrap.interface.ts +39 -0
- package/core/interface/context.interface.ts +9 -0
- package/core/interface/inventory.interface.ts +88 -0
- package/core/interface/nexine.interface.ts +4 -0
- package/core/interface/prisma.interface.ts +8844 -0
- package/core/interface/product.interface.ts +111 -0
- package/core/raxon.context.tsx +256 -0
- package/core/schema/checkout.schema.ts +103 -0
- package/core/util/basket.item.display.ts +19 -0
- package/core/util/category.nav.ts +46 -0
- package/core/util/client-ip.ts +35 -0
- package/core/util/collection.util.ts +433 -0
- package/core/util/fetch.bootstrap.ts +21 -0
- package/core/util/garanti-payment.ts +5 -0
- package/core/util/nexine.axios.tsx +104 -0
- package/core/util/no-cache.ts +6 -0
- package/core/util/util.ts +191 -0
- package/core/view/view.checkout.tsx +1964 -0
- package/dist/core/view/view.checkout.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -3
- package/tailwind.css +11 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useRaxon } from '@/core/raxon.context';
|
|
5
|
+
import { useCart } from '@/core/feature/cart/hook/use.cart';
|
|
6
|
+
import { usePromoCode } from '@/core/feature/promo-code/hook/use.promo.code';
|
|
7
|
+
import { PromoCode, Status } from '@/core/interface/prisma.interface';
|
|
8
|
+
|
|
9
|
+
const CheckCircle2 = (props: any) => (
|
|
10
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
|
11
|
+
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><polyline points="9 11 12 14 22 4" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const Loader2 = (props: any) => (
|
|
16
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
|
17
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const X = (props: any) => (
|
|
22
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
|
23
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function formatTRY(amount: number) {
|
|
28
|
+
return new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY', maximumFractionDigits: 0 }).format(amount);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CartPromoCodeSectionProps = {
|
|
32
|
+
variant?: 'cart' | 'checkout';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function CartPromoCodeSection({ variant = 'cart' }: CartPromoCodeSectionProps) {
|
|
36
|
+
const { cart, promoCode, setPromoCode } = useRaxon();
|
|
37
|
+
const { mutate: updateCart, isPending: isUpdatingCart } = useCart().update();
|
|
38
|
+
const { mutate: findPromoCode, isPending: isFindingPromoCode } = usePromoCode().findByCode();
|
|
39
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
40
|
+
const [promoInput, setPromoInput] = useState('');
|
|
41
|
+
const [error, setError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const appliedCode = cart?.promoCode?.code ?? promoCode?.code;
|
|
44
|
+
const isApplied = Boolean(cart?.promoCode?.id ?? promoCode?.id);
|
|
45
|
+
const isLoading = isFindingPromoCode || isUpdatingCart;
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (cart?.promoCode?.id) {
|
|
49
|
+
setPromoInput(cart.promoCode.code);
|
|
50
|
+
setIsOpen(true);
|
|
51
|
+
}
|
|
52
|
+
}, [cart?.promoCode?.id, cart?.promoCode?.code]);
|
|
53
|
+
|
|
54
|
+
const applyPromoCode = useCallback(
|
|
55
|
+
(foundPromoCode: PromoCode) => {
|
|
56
|
+
if (!cart?.id) {
|
|
57
|
+
setError('Sepet bilgisi bulunamadı.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (foundPromoCode.status === Status.ARCHIVED) {
|
|
61
|
+
setError('Bu promosyon kodu artık geçerli değil.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const currentBasketAmount = cart.info?.basePrice?.pay ?? cart.info?.basePrice?.total ?? 0;
|
|
65
|
+
if (foundPromoCode.basketLimit > 0 && currentBasketAmount < foundPromoCode.basketLimit) {
|
|
66
|
+
setError(
|
|
67
|
+
`Bu kodu kullanabilmek için minimum ${formatTRY(foundPromoCode.basketLimit)} tutarında alışveriş yapmalısınız. Mevcut tutarınız: ${formatTRY(currentBasketAmount)}`
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
updateCart(
|
|
72
|
+
{
|
|
73
|
+
id: cart.id,
|
|
74
|
+
promoCodeId: foundPromoCode.id,
|
|
75
|
+
campaignId: cart.campaign?.id ?? undefined,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
setPromoCode(foundPromoCode);
|
|
80
|
+
setPromoInput(foundPromoCode.code);
|
|
81
|
+
setError(null);
|
|
82
|
+
setIsOpen(true);
|
|
83
|
+
},
|
|
84
|
+
onError: () => {
|
|
85
|
+
setError('Promosyon kodu sepete eklenemedi.');
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
[cart, setPromoCode, updateCart]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const handleApply = () => {
|
|
94
|
+
const trimmed = promoInput.trim();
|
|
95
|
+
if (!trimmed) {
|
|
96
|
+
setError('Lütfen bir promosyon kodu girin.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
setError(null);
|
|
100
|
+
findPromoCode(trimmed, {
|
|
101
|
+
onSuccess: applyPromoCode,
|
|
102
|
+
onError: () => {
|
|
103
|
+
setError('Geçersiz promosyon kodu.');
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleClear = () => {
|
|
109
|
+
if (!cart?.id) return;
|
|
110
|
+
updateCart(
|
|
111
|
+
{
|
|
112
|
+
id: cart.id,
|
|
113
|
+
promoCodeId: null,
|
|
114
|
+
campaignId: cart.campaign?.id ?? undefined,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
onSuccess: () => {
|
|
118
|
+
setPromoCode(null);
|
|
119
|
+
setPromoInput('');
|
|
120
|
+
setError(null);
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const toggleLabelClass =
|
|
127
|
+
variant === 'checkout'
|
|
128
|
+
? 'text-left text-xs font-black uppercase tracking-widest text-gray-600 underline-offset-2 transition-colors hover:text-[#CF0A2C] hover:underline'
|
|
129
|
+
: 'text-left text-xs font-semibold text-gray-500 underline-offset-2 transition-colors hover:text-gray-900 hover:underline';
|
|
130
|
+
|
|
131
|
+
const inputClass = (hasError: boolean, applied: boolean) =>
|
|
132
|
+
`w-full rounded-lg border px-3 py-2 text-sm font-medium uppercase tracking-wide outline-none transition focus:ring-2 focus:ring-gray-900/10 ${
|
|
133
|
+
applied
|
|
134
|
+
? 'border-green-500 bg-green-50 pr-9'
|
|
135
|
+
: hasError
|
|
136
|
+
? 'border-red-300 bg-red-50'
|
|
137
|
+
: 'border-gray-200 bg-white'
|
|
138
|
+
}`;
|
|
139
|
+
|
|
140
|
+
const applyButtonClass =
|
|
141
|
+
variant === 'checkout'
|
|
142
|
+
? 'shrink-0 rounded-lg bg-gray-900 px-4 py-2 text-xs font-bold uppercase tracking-widest text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50'
|
|
143
|
+
: 'shrink-0 rounded-full bg-black px-4 py-2 text-[10px] font-black uppercase tracking-[0.15em] text-white transition hover:bg-[#CF0A2C] disabled:cursor-not-allowed disabled:opacity-50';
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex flex-col gap-2">
|
|
147
|
+
{!isOpen ? (
|
|
148
|
+
<button type="button" onClick={() => setIsOpen(true)} className={toggleLabelClass}>
|
|
149
|
+
İndirim kodu ekle
|
|
150
|
+
</button>
|
|
151
|
+
) : (
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
<span className="text-xs font-medium text-gray-600">İndirim kodu</span>
|
|
154
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
155
|
+
<div className="relative flex-1 min-w-0">
|
|
156
|
+
<input
|
|
157
|
+
type="text"
|
|
158
|
+
value={promoInput}
|
|
159
|
+
onChange={e => {
|
|
160
|
+
setPromoInput(e.target.value.toUpperCase());
|
|
161
|
+
setError(null);
|
|
162
|
+
}}
|
|
163
|
+
onKeyDown={e => {
|
|
164
|
+
if (e.key === 'Enter') {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
handleApply();
|
|
167
|
+
}
|
|
168
|
+
}}
|
|
169
|
+
placeholder="Kodu girin"
|
|
170
|
+
disabled={isApplied || isLoading}
|
|
171
|
+
className={inputClass(Boolean(error), isApplied)}
|
|
172
|
+
/>
|
|
173
|
+
{isApplied ? (
|
|
174
|
+
<CheckCircle2 className="absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-green-500" />
|
|
175
|
+
) : null}
|
|
176
|
+
</div>
|
|
177
|
+
{isApplied ? (
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={handleClear}
|
|
181
|
+
disabled={isLoading}
|
|
182
|
+
className="flex h-9 w-full sm:w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 text-gray-400 transition hover:border-red-200 hover:bg-red-50 hover:text-red-500 disabled:opacity-50"
|
|
183
|
+
title="Kodu kaldır"
|
|
184
|
+
>
|
|
185
|
+
<X className="h-4 w-4" />
|
|
186
|
+
</button>
|
|
187
|
+
) : (
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={handleApply}
|
|
191
|
+
disabled={!promoInput.trim() || isLoading}
|
|
192
|
+
className={`${applyButtonClass} w-full sm:w-auto`}
|
|
193
|
+
>
|
|
194
|
+
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Uygula'}
|
|
195
|
+
</button>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
{error ? <p className="text-xs text-red-600">{error}</p> : null}
|
|
199
|
+
{isApplied && appliedCode ? (
|
|
200
|
+
<p className="text-xs text-green-600">
|
|
201
|
+
<span className="font-semibold">{appliedCode}</span> kodu uygulandı.
|
|
202
|
+
</p>
|
|
203
|
+
) : null}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { nexineAxios } from '@/core/util/nexine.axios';
|
|
2
|
+
import { BankTransferCode, Cart, CartItem } from '@/core/interface/prisma.interface';
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { globalEmitter, GLOBAL_EVENTS } from '@/core/util/util';
|
|
5
|
+
import { BasketSummaryInterface } from '@/core/interface/basket.interface';
|
|
6
|
+
import { mergeCartWithOptimisticExtras, applyBasketInsertResponse, readCartCache, patchCartCache, CART_QUERY_KEY } from '@/core/feature/cart/util/cart-optimistic';
|
|
7
|
+
import { IData } from '@/core/interface/nexine.interface';
|
|
8
|
+
export type BasketItemBulkInsertInput = {
|
|
9
|
+
id?: string;
|
|
10
|
+
productId?: string;
|
|
11
|
+
productUnitId?: string;
|
|
12
|
+
variantId?: string | null;
|
|
13
|
+
quantity?: number;
|
|
14
|
+
type?: 'increment' | 'decrement' | 'set';
|
|
15
|
+
};
|
|
16
|
+
export type BasketAddressInput = {
|
|
17
|
+
title?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
firstName?: string;
|
|
20
|
+
lastName?: string;
|
|
21
|
+
phoneNumber?: string;
|
|
22
|
+
postalCode?: string;
|
|
23
|
+
administrativeAreaLevel1?: string;
|
|
24
|
+
administrativeAreaLevel2?: string;
|
|
25
|
+
administrativeAreaLevel3?: string;
|
|
26
|
+
fullAddress?: string;
|
|
27
|
+
streetName?: string;
|
|
28
|
+
buildingName?: string | null;
|
|
29
|
+
apartmentNumber?: string | null;
|
|
30
|
+
companyName?: string;
|
|
31
|
+
taxNumber?: string;
|
|
32
|
+
country?: string;
|
|
33
|
+
countryCode?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const useCart = () => {
|
|
37
|
+
const queryClient = useQueryClient();
|
|
38
|
+
|
|
39
|
+
const handleUnauthorizedError = (error: any) => {
|
|
40
|
+
if (error?.response?.status === 401) {
|
|
41
|
+
globalEmitter.emit(GLOBAL_EVENTS.SHOW_LOGIN_MODAL, {
|
|
42
|
+
message: 'Oturum süreniz dolmuş. Lütfen tekrar giriş yapın.',
|
|
43
|
+
source: 'cart_operation',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
fetch: (params?: { promoCodeId?: string; campaignId?: string; deliveryMethodId?: string; isEnabled: boolean }) => {
|
|
50
|
+
return useQuery({
|
|
51
|
+
queryKey: [...CART_QUERY_KEY],
|
|
52
|
+
staleTime: 15_000,
|
|
53
|
+
refetchOnWindowFocus: false,
|
|
54
|
+
enabled: params?.isEnabled,
|
|
55
|
+
queryFn: async () => {
|
|
56
|
+
const response = await nexineAxios.get<BasketSummaryInterface>('/customer/basket/me', {
|
|
57
|
+
params: {
|
|
58
|
+
promoCodeId: params?.promoCodeId,
|
|
59
|
+
campaignId: params?.campaignId,
|
|
60
|
+
deliveryMethodId: params?.deliveryMethodId,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const serverCart = response.data;
|
|
65
|
+
const cachedCart = readCartCache(queryClient);
|
|
66
|
+
|
|
67
|
+
if (!serverCart?.items?.length) {
|
|
68
|
+
return cachedCart?.items?.length ? cachedCart : serverCart;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return mergeCartWithOptimisticExtras(serverCart, cachedCart ?? undefined);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
insert: () => {
|
|
77
|
+
return useMutation({
|
|
78
|
+
mutationFn: async (data: any) => {
|
|
79
|
+
const response = await nexineAxios.post('/customer/basket/me/item', data);
|
|
80
|
+
return response.data;
|
|
81
|
+
},
|
|
82
|
+
onError: (error, _newData, context: any) => {
|
|
83
|
+
if (context?.previousCarts) {
|
|
84
|
+
context.previousCarts.forEach(([key, value]: any) => {
|
|
85
|
+
queryClient.setQueryData(key, value);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
handleUnauthorizedError(error);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
generatePaymentUrl: () => {
|
|
94
|
+
return useMutation({
|
|
95
|
+
mutationFn: async (data: any) => {
|
|
96
|
+
try {
|
|
97
|
+
var response = await nexineAxios.post('/customer/basket/pay/paytr', data);
|
|
98
|
+
return response.data;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.log(error);
|
|
101
|
+
handleUnauthorizedError(error);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
generateStripePaymentIntent: () => {
|
|
109
|
+
return useMutation({
|
|
110
|
+
mutationFn: async (data: any) => {
|
|
111
|
+
var response = await nexineAxios.post('/customer/basket/pay/stripe', data);
|
|
112
|
+
return response.data;
|
|
113
|
+
},
|
|
114
|
+
onError: handleUnauthorizedError,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
generateCashOnDeliveryPayment: () => {
|
|
119
|
+
return useMutation({
|
|
120
|
+
mutationFn: async (data: any) => {
|
|
121
|
+
var response = await nexineAxios.post('/customer/basket/pay/cash-on-delivery', data);
|
|
122
|
+
return response.data;
|
|
123
|
+
},
|
|
124
|
+
onError: handleUnauthorizedError,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
pay: () => {
|
|
129
|
+
return useMutation({
|
|
130
|
+
mutationFn: async (data?: { platform?: string; webReturnUrl?: string }) => {
|
|
131
|
+
const response = await nexineAxios.post('/customer/basket/me/pay', {
|
|
132
|
+
platform: data?.platform ?? 'web',
|
|
133
|
+
webReturnUrl: data?.webReturnUrl ?? window.location.href,
|
|
134
|
+
});
|
|
135
|
+
return response.data;
|
|
136
|
+
},
|
|
137
|
+
onError: handleUnauthorizedError,
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
createTransferCode: () => {
|
|
142
|
+
return useMutation({
|
|
143
|
+
mutationFn: async (data: { amount: number; bankAccountId: string }) => {
|
|
144
|
+
const response = await nexineAxios.post<{ bankTransferCode: BankTransferCode }>(
|
|
145
|
+
'/customer/payment/bank-transfer',
|
|
146
|
+
data
|
|
147
|
+
);
|
|
148
|
+
return response.data.bankTransferCode;
|
|
149
|
+
},
|
|
150
|
+
onError: handleUnauthorizedError,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
bulkInsert: () => {
|
|
154
|
+
return useMutation({
|
|
155
|
+
mutationFn: (data: BasketItemBulkInsertInput[]) => {
|
|
156
|
+
return nexineAxios.patch<IData<CartItem[]>>('/customer/basket/me/item', data);
|
|
157
|
+
},
|
|
158
|
+
onSuccess: () => {
|
|
159
|
+
queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
|
|
160
|
+
},
|
|
161
|
+
onError: handleUnauthorizedError,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
remove: () => {
|
|
165
|
+
return useMutation({
|
|
166
|
+
mutationFn: (id: string) => {
|
|
167
|
+
return nexineAxios.delete<CartItem>(`/customer/basket/me/item/${id}`);
|
|
168
|
+
},
|
|
169
|
+
onMutate: async (id) => {
|
|
170
|
+
await queryClient.cancelQueries({ queryKey: ['organization', 'cart'] });
|
|
171
|
+
const previousCarts = queryClient.getQueriesData({ queryKey: ['organization', 'cart'] });
|
|
172
|
+
|
|
173
|
+
queryClient.setQueriesData({ queryKey: CART_QUERY_KEY }, (old: any) => {
|
|
174
|
+
if (!old || !old.items) return old;
|
|
175
|
+
|
|
176
|
+
const removedItem = old.items.find((item: any) => String(item.id) === String(id));
|
|
177
|
+
const priceDiff = removedItem ? -Number(removedItem.linePay || 0) : 0;
|
|
178
|
+
const updatedItems = old.items.filter((item: any) => String(item.id) !== String(id));
|
|
179
|
+
|
|
180
|
+
const newGesamt = updatedItems.reduce((sum: number, item: any) => sum + Number(item.quantity || 0), 0);
|
|
181
|
+
const newTotal = Number(old.info?.payPrice?.total || 0) + priceDiff;
|
|
182
|
+
const newPay = Number(old.info?.payPrice?.pay || 0) + priceDiff;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
...old,
|
|
186
|
+
items: updatedItems,
|
|
187
|
+
info: {
|
|
188
|
+
...old.info,
|
|
189
|
+
count: {
|
|
190
|
+
...old.info?.count,
|
|
191
|
+
Gesamt: newGesamt,
|
|
192
|
+
Artikel: updatedItems.length,
|
|
193
|
+
},
|
|
194
|
+
payPrice: {
|
|
195
|
+
...old.info?.payPrice,
|
|
196
|
+
total: newTotal,
|
|
197
|
+
pay: newPay,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return { previousCarts };
|
|
204
|
+
},
|
|
205
|
+
onSuccess: () => {
|
|
206
|
+
queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
|
|
207
|
+
},
|
|
208
|
+
onError: (error, id, context: any) => {
|
|
209
|
+
if (context?.previousCarts) {
|
|
210
|
+
context.previousCarts.forEach(([key, value]: any) => {
|
|
211
|
+
queryClient.setQueryData(key, value);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
handleUnauthorizedError(error);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
update: () => {
|
|
220
|
+
return useMutation({
|
|
221
|
+
mutationFn: async (data: {
|
|
222
|
+
id: string;
|
|
223
|
+
email?: string;
|
|
224
|
+
invoiceAddressId?: string | null;
|
|
225
|
+
deliveryAddressId?: string | null;
|
|
226
|
+
deliveryMethodId?: string | null;
|
|
227
|
+
paymentMethodId?: string | null;
|
|
228
|
+
promoCodeId?: string | null;
|
|
229
|
+
campaignId?: string | null;
|
|
230
|
+
deliveryAddress?: BasketAddressInput;
|
|
231
|
+
invoiceAddress?: BasketAddressInput;
|
|
232
|
+
bankTransferCodeId?: string | null;
|
|
233
|
+
}) => {
|
|
234
|
+
const response = await nexineAxios.put<BasketSummaryInterface>('/customer/basket', data);
|
|
235
|
+
return response.data;
|
|
236
|
+
},
|
|
237
|
+
onSuccess: () => {
|
|
238
|
+
queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
|
|
239
|
+
},
|
|
240
|
+
onError: handleUnauthorizedError,
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
updateItem: () => {
|
|
245
|
+
return useMutation({
|
|
246
|
+
mutationFn: async (data: { id: string; quantity: number; productId?: string }) => {
|
|
247
|
+
const response = await nexineAxios.put<BasketSummaryInterface>(`/customer/basket/me/item`, {
|
|
248
|
+
itemId: data.id,
|
|
249
|
+
quantity: Number(data.quantity || 0),
|
|
250
|
+
});
|
|
251
|
+
return response.data;
|
|
252
|
+
},
|
|
253
|
+
onSuccess: (data) => {
|
|
254
|
+
patchCartCache(queryClient, (old) => applyBasketInsertResponse(data, old));
|
|
255
|
+
},
|
|
256
|
+
onError: (error, _newData, context: any) => {
|
|
257
|
+
if (context?.previousCarts) {
|
|
258
|
+
context.previousCarts.forEach(([key, value]: any) => {
|
|
259
|
+
queryClient.setQueryData(key, value);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
handleUnauthorizedError(error);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PaymentProvider } from '@/core/interface/prisma.interface';
|
|
2
|
+
|
|
3
|
+
export type BasketPayResponse = {
|
|
4
|
+
status?: boolean | string;
|
|
5
|
+
success?: boolean;
|
|
6
|
+
info?: Record<string, unknown> | null;
|
|
7
|
+
data?: Record<string, unknown> | null;
|
|
8
|
+
provider?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function readString(source: Record<string, unknown> | null, ...keys: string[]): string | null {
|
|
12
|
+
if (!source) return null;
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
const value = source[key];
|
|
15
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseBasketPayResponse(response: unknown) {
|
|
21
|
+
const payload =
|
|
22
|
+
response && typeof response === 'object' ? (response as BasketPayResponse) : ({} as BasketPayResponse);
|
|
23
|
+
|
|
24
|
+
const ok =
|
|
25
|
+
payload.status === true ||
|
|
26
|
+
payload.status === 'true' ||
|
|
27
|
+
payload.success === true;
|
|
28
|
+
|
|
29
|
+
const info =
|
|
30
|
+
payload.info && typeof payload.info === 'object' ? (payload.info as Record<string, unknown>) : null;
|
|
31
|
+
const data =
|
|
32
|
+
payload.data && typeof payload.data === 'object' ? (payload.data as Record<string, unknown>) : null;
|
|
33
|
+
const root = payload as Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
const provider =
|
|
36
|
+
readString(info, 'provider', 'paymentProvider') ||
|
|
37
|
+
readString(data, 'provider', 'paymentProvider') ||
|
|
38
|
+
readString(root, 'provider', 'paymentProvider');
|
|
39
|
+
|
|
40
|
+
const html =
|
|
41
|
+
readString(info, 'html', 'formHtml', 'paymentHtml') ||
|
|
42
|
+
readString(data, 'html', 'formHtml', 'paymentHtml');
|
|
43
|
+
|
|
44
|
+
const transactionId =
|
|
45
|
+
readString(info, 'transactionId', 'transaction_id', 'id') ||
|
|
46
|
+
readString(data, 'transactionId', 'transaction_id', 'id');
|
|
47
|
+
|
|
48
|
+
const token = readString(info, 'token') || readString(data, 'token');
|
|
49
|
+
const orderId = readString(info, 'orderId', 'order_id') || readString(data, 'orderId', 'order_id');
|
|
50
|
+
const url = readString(info, 'url', 'redirectUrl') || readString(data, 'url', 'redirectUrl');
|
|
51
|
+
|
|
52
|
+
return { ok, provider, html, transactionId, token, orderId, url };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isGarantiPayResponse(provider: string | null) {
|
|
56
|
+
return provider === PaymentProvider.GARANTI || provider?.toUpperCase() === 'GARANTI';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Provider alanı eksik gelse bile Garanti HTML yanıtını tanı */
|
|
60
|
+
export function isGarantiPayFlow(
|
|
61
|
+
provider: string | null,
|
|
62
|
+
html: string | null,
|
|
63
|
+
transactionId: string | null
|
|
64
|
+
) {
|
|
65
|
+
if (!html || !transactionId) return false;
|
|
66
|
+
return isGarantiPayResponse(provider) || /<form/i.test(html);
|
|
67
|
+
}
|