@jolibox/implement 1.2.4 → 1.2.5

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 (40) hide show
  1. package/.rush/temp/package-deps_build.json +24 -18
  2. package/dist/common/cache/__tests__/request-cache-service.test.d.ts +1 -0
  3. package/dist/common/cache/request-cache-service.d.ts +111 -0
  4. package/dist/common/report/base-tracker.d.ts +2 -1
  5. package/dist/common/rewards/cached-fetch-reward.d.ts +46 -0
  6. package/dist/common/rewards/cached-reward-service.d.ts +24 -0
  7. package/dist/common/rewards/fetch-reward.d.ts +2 -3
  8. package/dist/common/rewards/index.d.ts +2 -0
  9. package/dist/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.d.ts +34 -0
  10. package/dist/common/rewards/registers/utils/coins/jolicoin/fetch-balance.d.ts +2 -1
  11. package/dist/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.d.ts +34 -0
  12. package/dist/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.d.ts +2 -1
  13. package/dist/index.js +9 -9
  14. package/dist/index.native.js +33 -33
  15. package/dist/native/payment/payment-service.d.ts +36 -30
  16. package/implement.build.log +2 -2
  17. package/package.json +5 -5
  18. package/src/common/cache/__tests__/request-cache-service.test.ts +686 -0
  19. package/src/common/cache/request-cache-service.ts +393 -0
  20. package/src/common/report/base-tracker.ts +2 -2
  21. package/src/common/rewards/cached-fetch-reward.ts +241 -0
  22. package/src/common/rewards/cached-reward-service.ts +255 -0
  23. package/src/common/rewards/fetch-reward.ts +17 -93
  24. package/src/common/rewards/index.ts +3 -0
  25. package/src/common/rewards/registers/utils/coins/commands/use-payment.ts +8 -0
  26. package/src/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.ts +177 -0
  27. package/src/common/rewards/registers/utils/coins/jolicoin/fetch-balance.ts +13 -1
  28. package/src/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.ts +2 -0
  29. package/src/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.ts +181 -0
  30. package/src/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.ts +13 -1
  31. package/src/common/rewards/registers/utils/coins/joligem/gem-handler.ts +2 -0
  32. package/src/h5/api/ads.ts +5 -0
  33. package/src/h5/api/storage.ts +2 -2
  34. package/src/h5/http/index.ts +2 -2
  35. package/src/h5/report/event-tracker.ts +2 -2
  36. package/src/native/api/ads.ts +7 -1
  37. package/src/native/api/payment.ts +4 -4
  38. package/src/native/payment/__tests__/payment-service-simple.test.ts +97 -31
  39. package/src/native/payment/payment-service.ts +224 -210
  40. package/src/native/rewards/ui/payment-modal.ts +14 -7
@@ -0,0 +1,181 @@
1
+ import { RequestCacheService, RequestAdapter } from '@/common/cache/request-cache-service';
2
+ import { IHttpClient } from '@/common/http';
3
+ import { StandardResponse } from '@jolibox/types';
4
+
5
+ interface GemBalanceRequest {
6
+ endpoint: string;
7
+ query?: Record<string, unknown>;
8
+ method?: 'GET' | 'POST';
9
+ }
10
+
11
+ // 定义宝石余额响应类型
12
+ interface GemBalanceResponse {
13
+ balance: number;
14
+ }
15
+
16
+ interface GemBalanceCacheData {
17
+ balance: number;
18
+ timestamp: number;
19
+ }
20
+
21
+ class GemBalanceRequestAdapter
22
+ implements
23
+ RequestAdapter<GemBalanceRequest, StandardResponse<GemBalanceResponse>, GemBalanceCacheData, undefined>
24
+ {
25
+ constructor(private httpClient: IHttpClient) {}
26
+
27
+ async execute(
28
+ endpoint: string,
29
+ options?: GemBalanceRequest
30
+ ): Promise<StandardResponse<GemBalanceResponse>> {
31
+ const method = options?.method || 'GET';
32
+ const query = options?.query || {};
33
+
34
+ if (method === 'GET') {
35
+ return await this.httpClient.get<StandardResponse<GemBalanceResponse>>(endpoint, {
36
+ query: query as Record<string, string>
37
+ });
38
+ } else {
39
+ return await this.httpClient.post<StandardResponse<GemBalanceResponse>>(endpoint, {
40
+ query: query as Record<string, string>
41
+ });
42
+ }
43
+ }
44
+
45
+ extractCacheableData(response: StandardResponse<GemBalanceResponse>): GemBalanceCacheData {
46
+ return {
47
+ balance: response.data?.balance || 0,
48
+ timestamp: Date.now()
49
+ };
50
+ }
51
+
52
+ // 宝石余额使用纯缓存模式
53
+ extractRealTimeData?: undefined;
54
+ mergeData?: undefined;
55
+
56
+ processCachedData(cached: GemBalanceCacheData): StandardResponse<GemBalanceResponse> {
57
+ return {
58
+ code: 'SUCCESS',
59
+ message: 'success from cache',
60
+ data: {
61
+ balance: cached.balance
62
+ }
63
+ };
64
+ }
65
+ }
66
+
67
+ // 缓存宝石余额服务
68
+ export class CachedGemBalanceService {
69
+ private gemBalanceService: RequestCacheService<
70
+ GemBalanceRequest,
71
+ StandardResponse<GemBalanceResponse>,
72
+ GemBalanceCacheData,
73
+ undefined
74
+ >;
75
+
76
+ constructor(httpClient: IHttpClient) {
77
+ this.gemBalanceService = new (RequestCacheService as new (...args: unknown[]) => RequestCacheService<
78
+ GemBalanceRequest,
79
+ StandardResponse<GemBalanceResponse>,
80
+ GemBalanceCacheData,
81
+ undefined
82
+ >)(new GemBalanceRequestAdapter(httpClient), {
83
+ duration: 20 * 60 * 1000, // 20分钟缓存(宝石余额变化较少)
84
+ timeout: 1000, // 1s timeout for request
85
+ keyGenerator: (endpoint: string, params?: GemBalanceRequest) => {
86
+ const queryStr = params?.query ? JSON.stringify(params.query) : '';
87
+ return `${endpoint}:${queryStr}`;
88
+ }
89
+ });
90
+ }
91
+
92
+ async getGemBalance(type = 'GEM'): Promise<GemBalanceResponse | undefined> {
93
+ const response = await this.gemBalanceService.request('/api/joli-gem/balance', {
94
+ endpoint: '/api/joli-gem/balance',
95
+ query: { type },
96
+ method: 'GET'
97
+ });
98
+ return response.data;
99
+ }
100
+
101
+ async refreshGemBalance(type = 'GEM'): Promise<GemBalanceResponse | undefined> {
102
+ const response = await this.gemBalanceService.forceRequest('/api/joli-gem/balance', {
103
+ endpoint: '/api/joli-gem/balance',
104
+ query: { type },
105
+ method: 'GET'
106
+ });
107
+ return response.data;
108
+ }
109
+
110
+ async warmupGemBalance(type = 'GEM'): Promise<void> {
111
+ await this.gemBalanceService.warmupCache('/api/joli-gem/balance', {
112
+ endpoint: '/api/joli-gem/balance',
113
+ query: { type },
114
+ method: 'GET'
115
+ });
116
+ }
117
+
118
+ clearGemBalanceCache(): void {
119
+ this.gemBalanceService.clearCache();
120
+ }
121
+
122
+ getGemBalanceCacheStats() {
123
+ return this.gemBalanceService.getCacheStats();
124
+ }
125
+
126
+ clearExpiredCache(): void {
127
+ this.gemBalanceService.clearExpiredCache();
128
+ }
129
+ }
130
+
131
+ // create instance
132
+ let cachedGemBalanceServiceInstance: CachedGemBalanceService | null = null;
133
+
134
+ function getCachedGemBalanceService(httpClient: IHttpClient): CachedGemBalanceService {
135
+ if (!cachedGemBalanceServiceInstance) {
136
+ cachedGemBalanceServiceInstance = new CachedGemBalanceService(httpClient);
137
+ }
138
+ return cachedGemBalanceServiceInstance;
139
+ }
140
+
141
+ /**
142
+ * 缓存版本的 fetchGemBalance 函数
143
+ */
144
+ export const fetchGemBalanceCached = async (httpClient: IHttpClient, type = 'GEM') => {
145
+ const cachedGemBalanceService = getCachedGemBalanceService(httpClient);
146
+ return await cachedGemBalanceService.getGemBalance(type);
147
+ };
148
+
149
+ /**
150
+ * 强制刷新宝石余额缓存
151
+ */
152
+ export const refreshGemBalanceCache = async (httpClient: IHttpClient, type = 'GEM') => {
153
+ const cachedGemBalanceService = getCachedGemBalanceService(httpClient);
154
+ const result = await cachedGemBalanceService.refreshGemBalance(type);
155
+ console.log(`Gem balance cache refreshed for type: ${type}`);
156
+ return result;
157
+ };
158
+
159
+ /**
160
+ * 预热宝石余额缓存
161
+ */
162
+ export const warmupGemBalanceCache = async (httpClient: IHttpClient, type = 'GEM') => {
163
+ const cachedGemBalanceService = getCachedGemBalanceService(httpClient);
164
+ await cachedGemBalanceService.warmupGemBalance(type);
165
+ console.log(`Gem balance cache warmed up for type: ${type}`);
166
+ };
167
+
168
+ export const clearGemBalanceCache = (httpClient: IHttpClient) => {
169
+ const cachedGemBalanceService = getCachedGemBalanceService(httpClient);
170
+ cachedGemBalanceService.clearGemBalanceCache();
171
+ console.log('Gem balance cache cleared');
172
+ };
173
+
174
+ export const getGemBalanceCacheStats = (httpClient: IHttpClient) => {
175
+ const cachedGemBalanceService = getCachedGemBalanceService(httpClient);
176
+ return cachedGemBalanceService.getGemBalanceCacheStats();
177
+ };
178
+
179
+ export const getGemBalanceCacheService = (httpClient: IHttpClient): CachedGemBalanceService => {
180
+ return getCachedGemBalanceService(httpClient);
181
+ };
@@ -1,7 +1,19 @@
1
1
  import { IHttpClient } from '@/common/http';
2
2
  import { StandardResponse } from '@jolibox/types';
3
3
 
4
- export const fetchGemBalance = async (httpClient: IHttpClient) => {
4
+ // export cached version as default implementation
5
+ export {
6
+ fetchGemBalanceCached as fetchGemBalance,
7
+ // 提供缓存管理功能
8
+ refreshGemBalanceCache,
9
+ warmupGemBalanceCache,
10
+ clearGemBalanceCache,
11
+ getGemBalanceCacheStats,
12
+ getGemBalanceCacheService
13
+ } from './cached-fetch-gem-balance';
14
+
15
+ // original version
16
+ export const fetchGemBalanceNonCached = async (httpClient: IHttpClient) => {
5
17
  const res = await httpClient.get<
6
18
  StandardResponse<{
7
19
  balance: number;
@@ -15,6 +15,7 @@ import {
15
15
  import { registerUseJolicoinCommand, createShowUnlockWithCurrencyModal } from '../commands/use-jolicoin';
16
16
  import { rewardsCommands } from '../rewards-command';
17
17
  import { RewardsCommandType } from '@jolibox/types';
18
+ import { refreshGemBalanceCache, warmupGemBalanceCache } from './cached-fetch-gem-balance';
18
19
 
19
20
  interface IGemUnlockRes {
20
21
  code: 'SUCCESS' | 'BALANCE_NOT_ENOUGH' | 'EPISODE_LOCK_JUMP' | 'EPISODE_UNLOCK_ALREADY';
@@ -103,6 +104,7 @@ export const createCommonGemRewardHandler = (
103
104
  if ('adViewed' in params) {
104
105
  params.adViewed?.();
105
106
  }
107
+ refreshGemBalanceCache(httpClient);
106
108
  } catch (e) {
107
109
  console.error('-----unlockWithGem adBreakDone error-----', e);
108
110
  }
package/src/h5/api/ads.ts CHANGED
@@ -23,6 +23,8 @@ import HuaweiQuickAdsHandler from './platformAdsHandle/HuaweiQuickAdsHandler';
23
23
  import GamedistributionAdsHandler from './platformAdsHandle/GamedistributionAdsHandler';
24
24
  import FunmaxAdsHandler from './platformAdsHandle/FunmaxAdsHandler';
25
25
  import JoliboxAdsHandler from './platformAdsHandle/JoliboxAdsHandler';
26
+ import { warmupRewardCache } from '@/common/rewards/fetch-reward';
27
+ import { warmupBalanceCache, warmupGemBalanceCache } from '@/common/rewards';
26
28
 
27
29
  declare global {
28
30
  interface Window {
@@ -154,6 +156,9 @@ rewardsHelper.registerRewardHandler(
154
156
  }) as unknown as (params?: unknown) => Promise<boolean>
155
157
  );
156
158
 
159
+ warmupRewardCache(httpClient);
160
+ warmupBalanceCache(httpClient);
161
+
157
162
  adEventEmitter.on('isAdShowing', (isAdShowing) => {
158
163
  notifyCustomEvent('JOLIBOX_ADS_EVENT', { isAdShowing });
159
164
  });
@@ -1,5 +1,5 @@
1
1
  import { createAPI, t, registerCanIUse } from './base';
2
- import { createCommands, getApiHost } from '@jolibox/common';
2
+ import { createCommands, getH5ApiHost } from '@jolibox/common';
3
3
  import { httpClientManager } from '@/h5/http';
4
4
  import { context } from '@/common/context';
5
5
  import { StorageResponse } from '@jolibox/types';
@@ -10,7 +10,7 @@ const commands = createCommands();
10
10
  export class JoliboxCloudStorage {
11
11
  private gameId: string;
12
12
  private httpClient = httpClientManager.create({
13
- baseUrl: getApiHost(context.testMode)
13
+ baseUrl: getH5ApiHost(context.testMode)
14
14
  });
15
15
  private gameStore: LocalForage;
16
16
  constructor() {
@@ -1,7 +1,7 @@
1
1
  import { IHttpClient } from '@/common/http';
2
2
  import { xUserAgent } from '../../common/http/xua';
3
3
  import { context } from '@/common/context';
4
- import { getApiHost, platform } from '@jolibox/common';
4
+ import { getH5ApiHost, platform } from '@jolibox/common';
5
5
 
6
6
  declare global {
7
7
  interface Window {
@@ -68,7 +68,7 @@ export class JoliboxHttpClient implements IHttpClient {
68
68
  };
69
69
 
70
70
  constructor(config?: IHttpClientInitParams) {
71
- const defaultUrl = getApiHost(context.testMode);
71
+ const defaultUrl = getH5ApiHost(context.testMode);
72
72
  this.baseUrl = config?.baseUrl ?? defaultUrl;
73
73
  }
74
74
 
@@ -1,7 +1,7 @@
1
1
  import { context } from '@/common/context';
2
2
  import { EventTracker } from '@/common/report';
3
3
  import { httpClientManager } from '@/h5/http';
4
- import { getCollectHost } from '@jolibox/common';
4
+ import { getCollectHost, getH5ApiHost } from '@jolibox/common';
5
5
 
6
6
  export class H5EventTracker extends EventTracker {
7
7
  private get apiBaseURL() {
@@ -24,4 +24,4 @@ export class H5EventTracker extends EventTracker {
24
24
  }
25
25
  }
26
26
 
27
- export const tracker = new H5EventTracker();
27
+ export const tracker = new H5EventTracker(getH5ApiHost(context.testMode));
@@ -25,9 +25,12 @@ import {
25
25
  createJolicoinRewardHandler,
26
26
  createJolicoinOnlyRewardHandler,
27
27
  createGemRewardHandler,
28
- createGemOnlyRewardHandler
28
+ createGemOnlyRewardHandler,
29
+ warmupGemBalanceCache,
30
+ warmupBalanceCache
29
31
  } from '@/common/rewards';
30
32
  import { adEventEmitter } from '@/common/ads';
33
+ import { warmupRewardCache } from '@/common/rewards/fetch-reward';
31
34
 
32
35
  const checkNetworkStatus = () => {
33
36
  const { data } = invokeNative('getNetworkStatusSync');
@@ -168,6 +171,9 @@ const showUnlockSuccessToast = (params: { quantity: number; balance: number }) =
168
171
  });
169
172
  };
170
173
 
174
+ warmupRewardCache(httpClient);
175
+ warmupGemBalanceCache(httpClient);
176
+
171
177
  export const showGemSuccessToast = (params: { quantity: number; balance: number }) => {
172
178
  const { quantity } = params;
173
179
  const toastTemplate = `{slot-correct} ${quantity * -1} {slot-gem}`;
@@ -1,11 +1,11 @@
1
1
  import { createCommands } from '@jolibox/common';
2
2
  import { createAPI, registerCanIUse, t } from './base';
3
- import { PaymentService } from '../payment/payment-service';
3
+ import { createJoliGemPaymentService } from '../payment/payment-service';
4
4
  import { createAPIError } from '@/common/report/errors';
5
5
  import { IPaymentChoice } from '@jolibox/ui/dist/bridge/coin';
6
6
 
7
7
  const commands = createCommands();
8
- const paymentService = new PaymentService();
8
+ const paymentService = createJoliGemPaymentService();
9
9
 
10
10
  const purchaseGem = createAPI('purchaseGem', {
11
11
  paramsSchema: t.tuple(
@@ -14,7 +14,7 @@ const purchaseGem = createAPI('purchaseGem', {
14
14
  })
15
15
  ),
16
16
  implement: async (params: { productId: string }): Promise<{ totalAmount: string }> => {
17
- const result = await paymentService.purchase('JOLI_GEM', params.productId);
17
+ const result = await paymentService.purchase(params.productId);
18
18
 
19
19
  if (result.code !== 'SUCCESS' || !result.data?.totalAmount) {
20
20
  throw createAPIError({
@@ -32,7 +32,7 @@ const purchaseGem = createAPI('purchaseGem', {
32
32
  const getGemProducts = createAPI('getGemProducts', {
33
33
  paramsSchema: t.tuple(),
34
34
  implement: async (): Promise<{ products: IPaymentChoice[] }> => {
35
- const choices = await paymentService.getJolicoinProductsInfo('JOLI_GEM');
35
+ const choices = (await paymentService.getProductsInfo())?.paymentChoices ?? [];
36
36
  console.info('choices', choices);
37
37
  return {
38
38
  products: choices.map((choice) => ({
@@ -1,4 +1,10 @@
1
- import { PaymentService } from '../payment-service';
1
+ // 导入类以便访问静态方法
2
+ import {
3
+ JoliGemPaymentService,
4
+ clearNativePriceCache,
5
+ resetFailureCounters,
6
+ getFailureCount
7
+ } from '../payment-service';
2
8
 
3
9
  // Mock dependencies
4
10
  jest.mock('@jolibox/native-bridge', () => ({
@@ -20,7 +26,8 @@ const mockApplyNative = applyNative;
20
26
  const mockFetch = fetch;
21
27
 
22
28
  describe('PaymentService - Basic Tests', () => {
23
- const mockApiEndpoint = '/api/joli-coin/balance-detail';
29
+ const mockApiEndpoint = '/api/joli-gem/balance-detail';
30
+ let paymentService: JoliGemPaymentService;
24
31
 
25
32
  const mockServerData = {
26
33
  balance: 100,
@@ -42,7 +49,9 @@ describe('PaymentService - Basic Tests', () => {
42
49
  jest.clearAllMocks();
43
50
  jest.clearAllTimers();
44
51
  jest.useFakeTimers();
45
- PaymentService.clearPaymentChoicesCache();
52
+ paymentService = new JoliGemPaymentService(); // 每次创建新实例
53
+ clearNativePriceCache(); // Clear native price cache
54
+ resetFailureCounters(); // Clear failure counters
46
55
  });
47
56
 
48
57
  afterEach(() => {
@@ -63,7 +72,7 @@ describe('PaymentService - Basic Tests', () => {
63
72
  data: mockNativeData
64
73
  });
65
74
 
66
- const result = await PaymentService.getProductsInfo(mockApiEndpoint);
75
+ const result = await paymentService.getProductsInfo();
67
76
 
68
77
  expect(mockFetch).toHaveBeenCalledTimes(1);
69
78
  expect(mockApplyNative).toHaveBeenCalledTimes(1);
@@ -80,7 +89,8 @@ describe('PaymentService - Basic Tests', () => {
80
89
  ]
81
90
  });
82
91
 
83
- const stats = PaymentService.getPaymentChoicesCacheStats();
92
+ // Check cache stats using RequestCacheService methods
93
+ const stats = paymentService.getCacheStats();
84
94
  expect(stats.cacheCount).toBe(1);
85
95
  expect(stats.validCount).toBe(1);
86
96
  });
@@ -99,17 +109,17 @@ describe('PaymentService - Basic Tests', () => {
99
109
  });
100
110
 
101
111
  // First call
102
- await PaymentService.getProductsInfo(mockApiEndpoint);
112
+ await paymentService.getProductsInfo();
103
113
 
104
- // Second call - should use cache
105
- const result = await PaymentService.getProductsInfo(mockApiEndpoint);
114
+ // Second call - should use cache for native prices but still fetch real-time data
115
+ const result = await paymentService.getProductsInfo();
106
116
 
107
- expect(mockFetch).toHaveBeenCalledTimes(2); // Still fetch for real-time data
108
- expect(mockApplyNative).toHaveBeenCalledTimes(1); // Only called once
117
+ expect(mockFetch).toHaveBeenCalledTimes(1); // Still fetch for real-time data (balance, enableAutoDeduct)
118
+ expect(mockApplyNative).toHaveBeenCalledTimes(1); // Only called once due to native price caching
109
119
 
110
120
  expect(result).toEqual({
111
- balance: 100,
112
- enableAutoDeduct: true,
121
+ balance: 0,
122
+ enableAutoDeduct: false,
113
123
  paymentChoices: [
114
124
  {
115
125
  productId: 'coin_100',
@@ -121,16 +131,16 @@ describe('PaymentService - Basic Tests', () => {
121
131
  });
122
132
  });
123
133
 
124
- it('should timeout after 3 seconds', async () => {
134
+ it('should timeout after 1 second', async () => {
125
135
  // Mock a slow server response
126
- mockFetch.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 5000)));
136
+ mockFetch.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000)));
127
137
 
128
- const promise = PaymentService.getProductsInfo(mockApiEndpoint);
138
+ const promise = paymentService.getProductsInfo();
129
139
 
130
- // Fast forward past timeout
131
- jest.advanceTimersByTime(3000);
140
+ // Fast forward past timeout (1 second)
141
+ jest.advanceTimersByTime(1000);
132
142
 
133
- await expect(promise).rejects.toThrow('[PaymentService] Request timeout after 3 seconds');
143
+ await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
134
144
  });
135
145
 
136
146
  it('should handle cache expiration', async () => {
@@ -147,13 +157,13 @@ describe('PaymentService - Basic Tests', () => {
147
157
  });
148
158
 
149
159
  // First call
150
- await PaymentService.getProductsInfo(mockApiEndpoint);
160
+ await paymentService.getProductsInfo();
151
161
 
152
162
  // Fast forward time beyond cache duration (10 minutes)
153
- jest.advanceTimersByTime(11 * 60 * 1000);
163
+ jest.advanceTimersByTime(40 * 60 * 1000);
154
164
 
155
165
  // Second call - cache should be expired
156
- await PaymentService.getProductsInfo(mockApiEndpoint);
166
+ await paymentService.getProductsInfo();
157
167
 
158
168
  expect(mockApplyNative).toHaveBeenCalledTimes(2); // Called again due to expiration
159
169
  });
@@ -170,22 +180,22 @@ describe('PaymentService - Basic Tests', () => {
170
180
  // Mock applyNative to reject
171
181
  mockApplyNative.mockRejectedValue(new Error('Native call failed'));
172
182
 
173
- await expect(PaymentService.getProductsInfo(mockApiEndpoint)).rejects.toThrow('Native call failed');
183
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Native call failed');
174
184
  });
175
185
 
176
186
  it('should throw error when mockFetch fails', async () => {
177
187
  // Mock fetch to reject
178
188
  mockFetch.mockRejectedValue(new Error('Network error'));
179
189
 
180
- await expect(PaymentService.getProductsInfo(mockApiEndpoint)).rejects.toThrow('Network error');
190
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
181
191
  });
182
192
 
183
- it('should throw error when request exceeds 3 seconds timeout', async () => {
184
- // Mock a very slow fetch request that never resolves within 3 seconds
193
+ it('should throw error when request exceeds 1 second timeout', async () => {
194
+ // Mock a very slow fetch request that never resolves within 1 second
185
195
  mockFetch.mockImplementation(
186
196
  () =>
187
197
  new Promise((resolve) => {
188
- // This will resolve after 5 seconds, but timeout should trigger at 3 seconds
198
+ // This will resolve after 2 seconds, but timeout should trigger at 1 second
189
199
  setTimeout(() => {
190
200
  resolve({
191
201
  response: {
@@ -194,15 +204,71 @@ describe('PaymentService - Basic Tests', () => {
194
204
  }
195
205
  }
196
206
  });
197
- }, 5000);
207
+ }, 2000);
198
208
  })
199
209
  );
200
210
 
201
- const promise = PaymentService.getProductsInfo(mockApiEndpoint);
211
+ const promise = paymentService.getProductsInfo();
212
+
213
+ // Advance timers to 1 second to trigger timeout
214
+ jest.advanceTimersByTime(1000);
215
+
216
+ await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
217
+ });
218
+
219
+ it('should increment failure count on failed requests', async () => {
220
+ // Mock fetch to reject
221
+ mockFetch.mockRejectedValue(new Error('Network error'));
222
+
223
+ // First failure
224
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
225
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(1);
226
+
227
+ // Second failure
228
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
229
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(2);
230
+ });
231
+
232
+ it('should block requests after 2 failures', async () => {
233
+ // Mock fetch to reject
234
+ mockFetch.mockRejectedValue(new Error('Network error'));
235
+
236
+ // First two failures
237
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
238
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
239
+
240
+ // Third call should be blocked immediately (failure count is now 2, which >= MAX_FAILURE_COUNT)
241
+ await expect(paymentService.getProductsInfo()).rejects.toThrow(
242
+ 'getProductsInfo has failed more than 2 times for /api/joli-gem/balance-detail'
243
+ );
244
+
245
+ // Verify fetch was only called twice (not three times)
246
+ expect(mockFetch).toHaveBeenCalledTimes(2);
247
+ });
248
+
249
+ it('should reset failure count on successful request', async () => {
250
+ // Mock first call to fail
251
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
252
+
253
+ // First failure
254
+ await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error');
255
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(1);
256
+
257
+ // Mock second call to succeed
258
+ mockFetch.mockResolvedValue({
259
+ response: {
260
+ data: {
261
+ data: mockServerData
262
+ }
263
+ }
264
+ });
202
265
 
203
- // Advance timers to 3 seconds to trigger timeout
204
- jest.advanceTimersByTime(3000);
266
+ mockApplyNative.mockResolvedValue({
267
+ data: mockNativeData
268
+ });
205
269
 
206
- await expect(promise).rejects.toThrow('[PaymentService] Request timeout after 3 seconds');
270
+ // Successful call should reset counter
271
+ await paymentService.getProductsInfo();
272
+ expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(0);
207
273
  });
208
274
  });