@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.
Files changed (66) hide show
  1. package/core/component/general.image.tsx +86 -0
  2. package/core/context/cart.context.tsx +446 -0
  3. package/core/context/security.context.tsx +151 -0
  4. package/core/feature/address/api/places.api.ts +76 -0
  5. package/core/feature/address/form/address-search-input.tsx +125 -0
  6. package/core/feature/address/hook/use.addres.tsx +63 -0
  7. package/core/feature/address/hook/use.address-autocomplete.ts +116 -0
  8. package/core/feature/address/util/address.types.ts +38 -0
  9. package/core/feature/address/util/parse-google-place.ts +66 -0
  10. package/core/feature/analytic-event/analytic.event.api.ts +27 -0
  11. package/core/feature/analytic-event/analytic.event.context.tsx +180 -0
  12. package/core/feature/analytic-event/analytic.event.util.ts +42 -0
  13. package/core/feature/analytic-event/use.analytic.auto.tsx +114 -0
  14. package/core/feature/article/hook/use.article.tsx +33 -0
  15. package/core/feature/attribute/hook/use.attribute.tsx +24 -0
  16. package/core/feature/auth/hook/use.auth.tsx +141 -0
  17. package/core/feature/auth/modal/modal.auth.tsx +80 -0
  18. package/core/feature/auth/view/view.login.tsx +199 -0
  19. package/core/feature/auth/view/view.register.tsx +333 -0
  20. package/core/feature/bank-account/hook/use.bank.account.tsx +47 -0
  21. package/core/feature/brand/hook/use.brand.tsx +24 -0
  22. package/core/feature/cart/component/cart.order.summary.tsx +89 -0
  23. package/core/feature/cart/component/cart.promo.code.section.tsx +208 -0
  24. package/core/feature/cart/hook/use.cart.tsx +267 -0
  25. package/core/feature/cart/util/basket-pay.response.ts +67 -0
  26. package/core/feature/cart/util/cart-optimistic.ts +425 -0
  27. package/core/feature/cart/util/garanti-payment.ts +27 -0
  28. package/core/feature/collection/hook/use.collection.tsx +32 -0
  29. package/core/feature/delivery-method/hook/use.delivery.method.tsx +40 -0
  30. package/core/feature/delivery-method/util/checkout.delivery.method.ts +11 -0
  31. package/core/feature/faq/hook/use.faq.tsx +23 -0
  32. package/core/feature/favorite/hook/use.favorite.tsx +48 -0
  33. package/core/feature/form-submit/form/form.contact.tsx +118 -0
  34. package/core/feature/form-submit/hook/use.form.submit.tsx +16 -0
  35. package/core/feature/invoice/hook/use.invoice.tsx +51 -0
  36. package/core/feature/newsletter/hook/use.newsletter.tsx +124 -0
  37. package/core/feature/newsletter/modal/modal.newsletter.product.tsx +163 -0
  38. package/core/feature/order/hook/use.order.tsx +31 -0
  39. package/core/feature/payment-method/checkout.payment.options.ts +117 -0
  40. package/core/feature/payment-method/hook/use.payment.method.tsx +44 -0
  41. package/core/feature/product/hook/use.product.tsx +122 -0
  42. package/core/feature/profile/hook/use.profile.tsx +126 -0
  43. package/core/feature/promo-code/hook/use.promo.code.tsx +27 -0
  44. package/core/interface/basket.interface.ts +360 -0
  45. package/core/interface/bootstrap.interface.ts +39 -0
  46. package/core/interface/context.interface.ts +9 -0
  47. package/core/interface/inventory.interface.ts +88 -0
  48. package/core/interface/nexine.interface.ts +4 -0
  49. package/core/interface/prisma.interface.ts +8844 -0
  50. package/core/interface/product.interface.ts +111 -0
  51. package/core/raxon.context.tsx +256 -0
  52. package/core/schema/checkout.schema.ts +103 -0
  53. package/core/util/basket.item.display.ts +19 -0
  54. package/core/util/category.nav.ts +46 -0
  55. package/core/util/client-ip.ts +35 -0
  56. package/core/util/collection.util.ts +433 -0
  57. package/core/util/fetch.bootstrap.ts +21 -0
  58. package/core/util/garanti-payment.ts +5 -0
  59. package/core/util/nexine.axios.tsx +104 -0
  60. package/core/util/no-cache.ts +6 -0
  61. package/core/util/util.ts +191 -0
  62. package/core/view/view.checkout.tsx +1964 -0
  63. package/dist/core/view/view.checkout.js +2 -2
  64. package/dist/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +12 -3
  66. 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
+ }