@pvh-afl/core 1.1.1 → 1.1.3

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.
@@ -14,6 +14,7 @@ const capillary_types_1 = require("./capillary.types");
14
14
  let CapillaryCouponService = class CapillaryCouponService {
15
15
  authService;
16
16
  API_TIMEOUT = 5000;
17
+ COUPON_PAGE_SIZE = 100;
17
18
  constructor(authService) {
18
19
  this.authService = authService;
19
20
  }
@@ -21,58 +22,166 @@ let CapillaryCouponService = class CapillaryCouponService {
21
22
  const config = this.authService.getCapillaryConfig(brand);
22
23
  const token = await this.authService.getValidAccessToken(brand);
23
24
  const url = new URL(`${config.baseUrl}/v1.1/coupon/isredeemable`);
24
- url.searchParams.set('mobile', mobile);
25
- url.searchParams.set('code', couponCode);
26
- url.searchParams.set('details', 'extended');
27
- logger_1.logger.info('Validating Capillary coupon', { brand, couponCode, mobile: mobile.slice(-4) });
25
+ url.searchParams.set("mobile", mobile);
26
+ url.searchParams.set("code", couponCode);
27
+ url.searchParams.set("details", "extended");
28
+ logger_1.logger.info("Validating Capillary coupon", {
29
+ brand,
30
+ couponCode,
31
+ mobile: mobile.slice(-4),
32
+ });
28
33
  try {
29
34
  const response = await this.fetchWithTimeout(url.toString(), {
30
- method: 'GET',
35
+ method: "GET",
31
36
  headers: this.getHeaders(token, config),
32
37
  });
33
38
  if (!response.ok) {
34
39
  const errorText = await response.text();
35
- logger_1.logger.error('Capillary IsRedeemable API error', { brand, status: response.status, error: errorText });
40
+ logger_1.logger.error("Capillary IsRedeemable API error", {
41
+ brand,
42
+ status: response.status,
43
+ error: errorText,
44
+ });
36
45
  return {
37
46
  valid: false,
38
47
  couponCode,
39
- errorCode: 'API_ERROR',
48
+ errorCode: "API_ERROR",
40
49
  errorMessage: `Capillary API error: ${response.status}`,
41
50
  };
42
51
  }
43
52
  const data = (await response.json());
44
- return this.parseValidationResponse(data, couponCode, config.returnPolicyDays);
53
+ const result = this.parseValidationResponse(data, couponCode);
54
+ // Fetch coupon details to get valid_from
55
+ const couponDetails = await this.getCouponDetails(brand, couponCode);
56
+ // If error 709 (coupon not yet active), provide helpful message with valid_from
57
+ if (result.errorCode === "709" && couponDetails?.validFrom) {
58
+ const validFromDate = new Date(couponDetails.validFrom);
59
+ const formattedDate = validFromDate.toLocaleDateString("en-IN", {
60
+ year: "numeric",
61
+ month: "long",
62
+ day: "numeric",
63
+ });
64
+ logger_1.logger.info("Coupon not yet active", {
65
+ brand,
66
+ couponCode,
67
+ validFrom: couponDetails.validFrom,
68
+ });
69
+ return {
70
+ ...result,
71
+ validFrom: couponDetails.validFrom,
72
+ errorCode: capillary_types_1.COUPON_NOT_YET_ACTIVE_ERROR.errorCode,
73
+ errorMessage: capillary_types_1.COUPON_NOT_YET_ACTIVE_ERROR.errorMessage(formattedDate),
74
+ };
75
+ }
76
+ // If valid, check if coupon is not yet active based on valid_from
77
+ if (result.valid && couponDetails?.validFrom) {
78
+ const validFromDate = new Date(couponDetails.validFrom);
79
+ const now = new Date();
80
+ if (now < validFromDate) {
81
+ const formattedDate = validFromDate.toLocaleDateString("en-IN", {
82
+ year: "numeric",
83
+ month: "long",
84
+ day: "numeric",
85
+ });
86
+ logger_1.logger.info("Coupon valid_from is in future", {
87
+ brand,
88
+ couponCode,
89
+ validFrom: couponDetails.validFrom,
90
+ now: now.toISOString(),
91
+ });
92
+ return {
93
+ valid: false,
94
+ couponCode,
95
+ validFrom: couponDetails.validFrom,
96
+ errorCode: capillary_types_1.COUPON_NOT_YET_ACTIVE_ERROR.errorCode,
97
+ errorMessage: capillary_types_1.COUPON_NOT_YET_ACTIVE_ERROR.errorMessage(formattedDate),
98
+ };
99
+ }
100
+ }
101
+ // Include validFrom in successful response
102
+ return {
103
+ ...result,
104
+ validFrom: couponDetails?.validFrom,
105
+ };
45
106
  }
46
107
  catch (error) {
47
- logger_1.logger.error('Capillary coupon validation failed', {
108
+ logger_1.logger.error("Capillary coupon validation failed", {
48
109
  brand,
49
110
  couponCode,
50
- error: error instanceof Error ? error.message : 'Unknown',
111
+ error: error instanceof Error ? error.message : "Unknown",
51
112
  });
52
113
  return {
53
114
  valid: false,
54
115
  couponCode,
55
- errorCode: 'VALIDATION_FAILED',
56
- errorMessage: error instanceof Error ? error.message : 'Coupon validation failed',
116
+ errorCode: "VALIDATION_FAILED",
117
+ errorMessage: error instanceof Error ? error.message : "Coupon validation failed",
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * Get coupon details from Capillary /v1.1/coupon/get API.
123
+ * This is used to fetch the valid_from date which is not available in isredeemable API.
124
+ */
125
+ async getCouponDetails(brand, couponCode) {
126
+ const config = this.authService.getCapillaryConfig(brand);
127
+ const token = await this.authService.getValidAccessToken(brand);
128
+ const url = new URL(`${config.baseUrl}/v1.1/coupon/get`);
129
+ url.searchParams.set("code", couponCode);
130
+ logger_1.logger.info("Fetching Capillary coupon details", { brand, couponCode });
131
+ try {
132
+ const response = await this.fetchWithTimeout(url.toString(), {
133
+ method: "GET",
134
+ headers: this.getHeaders(token, config),
135
+ });
136
+ if (!response.ok) {
137
+ logger_1.logger.warn("Failed to fetch coupon details", {
138
+ brand,
139
+ couponCode,
140
+ status: response.status,
141
+ });
142
+ return null;
143
+ }
144
+ const data = (await response.json());
145
+ const coupon = data.response?.coupons?.coupon?.[0];
146
+ if (!coupon) {
147
+ logger_1.logger.warn("No coupon found in response", { brand, couponCode });
148
+ return null;
149
+ }
150
+ return {
151
+ validFrom: coupon.valid_from,
152
+ validTill: coupon.valid_till,
153
+ issuedOn: coupon.issued_on,
154
+ seriesId: coupon.series_id,
155
+ value: coupon.value,
57
156
  };
58
157
  }
158
+ catch (error) {
159
+ logger_1.logger.error("Error fetching coupon details", {
160
+ brand,
161
+ couponCode,
162
+ error: error instanceof Error ? error.message : "Unknown",
163
+ });
164
+ return null;
165
+ }
59
166
  }
60
167
  async createTransaction(brand, request) {
61
168
  const config = this.authService.getCapillaryConfig(brand);
62
169
  const token = await this.authService.getValidAccessToken(brand);
63
170
  const url = new URL(`${config.baseUrl}/v2/transactions`);
64
- url.searchParams.set('source', 'INSTORE');
65
- url.searchParams.set('identifierName', 'mobile');
66
- url.searchParams.set('identifierValue', request.mobile);
171
+ url.searchParams.set("source", "INSTORE");
172
+ url.searchParams.set("identifierName", "mobile");
173
+ url.searchParams.set("identifierValue", request.mobile);
67
174
  const body = {
68
175
  billAmount: request.billAmount,
69
176
  billNumber: request.billNumber,
70
177
  billingDate: request.billingDate,
71
- type: request.type ?? 'REGULAR',
178
+ type: request.type ?? "REGULAR",
72
179
  ...(request.returnType && { returnType: request.returnType }),
73
180
  ...(request.redemptionIds?.length && {
74
181
  redemptions: {
75
- couponRedemptions: request.redemptionIds.map((id) => ({ redemptionId: id })),
182
+ couponRedemptions: request.redemptionIds.map((id) => ({
183
+ redemptionId: id,
184
+ })),
76
185
  },
77
186
  }),
78
187
  lineItemsV2: request.lineItems.map((item) => ({
@@ -81,24 +190,28 @@ let CapillaryCouponService = class CapillaryCouponService {
81
190
  rate: item.rate,
82
191
  amount: item.amount,
83
192
  description: item.description,
84
- type: item.type ?? 'REGULAR',
193
+ type: item.type ?? "REGULAR",
85
194
  })),
86
195
  paymentModes: request.paymentModes,
87
196
  };
88
- logger_1.logger.info('Creating Capillary transaction', {
197
+ logger_1.logger.info("Creating Capillary transaction", {
89
198
  brand,
90
199
  billNumber: request.billNumber,
91
- type: request.type ?? 'REGULAR',
200
+ type: request.type ?? "REGULAR",
92
201
  });
93
202
  try {
94
203
  const response = await this.fetchWithTimeout(url.toString(), {
95
- method: 'POST',
204
+ method: "POST",
96
205
  headers: this.getHeaders(token, config),
97
206
  body: JSON.stringify(body),
98
207
  });
99
208
  if (!response.ok) {
100
209
  const errorText = await response.text();
101
- logger_1.logger.error('Capillary Transaction API error', { brand, status: response.status, error: errorText });
210
+ logger_1.logger.error("Capillary Transaction API error", {
211
+ brand,
212
+ status: response.status,
213
+ error: errorText,
214
+ });
102
215
  return {
103
216
  transactionId: 0,
104
217
  billNumber: request.billNumber,
@@ -113,10 +226,10 @@ let CapillaryCouponService = class CapillaryCouponService {
113
226
  transactionId: 0,
114
227
  billNumber: request.billNumber,
115
228
  errorCode: 500,
116
- errorMessage: 'No transaction in response',
229
+ errorMessage: "No transaction in response",
117
230
  };
118
231
  }
119
- logger_1.logger.info('Capillary transaction created', {
232
+ logger_1.logger.info("Capillary transaction created", {
120
233
  brand,
121
234
  billNumber: request.billNumber,
122
235
  transactionId: txn.id,
@@ -128,16 +241,18 @@ let CapillaryCouponService = class CapillaryCouponService {
128
241
  };
129
242
  }
130
243
  catch (error) {
131
- logger_1.logger.error('Capillary transaction creation failed', {
244
+ logger_1.logger.error("Capillary transaction creation failed", {
132
245
  brand,
133
246
  billNumber: request.billNumber,
134
- error: error instanceof Error ? error.message : 'Unknown',
247
+ error: error instanceof Error ? error.message : "Unknown",
135
248
  });
136
249
  return {
137
250
  transactionId: 0,
138
251
  billNumber: request.billNumber,
139
252
  errorCode: 500,
140
- errorMessage: error instanceof Error ? error.message : 'Transaction creation failed',
253
+ errorMessage: error instanceof Error
254
+ ? error.message
255
+ : "Transaction creation failed",
141
256
  };
142
257
  }
143
258
  }
@@ -150,21 +265,27 @@ let CapillaryCouponService = class CapillaryCouponService {
150
265
  const token = await this.authService.getValidAccessToken(brand);
151
266
  // Use mobile param directly - identifierName/identifierValue returns 400 error
152
267
  const url = new URL(`${config.baseUrl}/v2/customers/coupons`);
153
- url.searchParams.set('mobile', mobile);
154
- logger_1.logger.info('Getting coupons by transaction', { brand, transactionNumber });
268
+ url.searchParams.set("mobile", mobile);
269
+ logger_1.logger.info("Getting coupons by transaction", { brand, transactionNumber });
155
270
  try {
156
271
  const response = await this.fetchWithTimeout(url.toString(), {
157
- method: 'GET',
272
+ method: "GET",
158
273
  headers: this.getHeaders(token, config),
159
274
  });
160
275
  if (!response.ok) {
161
- logger_1.logger.error('Capillary Get Coupons API error', { brand, status: response.status });
276
+ logger_1.logger.error("Capillary Get Coupons API error", {
277
+ brand,
278
+ status: response.status,
279
+ });
162
280
  return [];
163
281
  }
164
282
  const data = (await response.json());
165
283
  // Check for API-level errors
166
284
  if (data.errors && data.errors.length > 0) {
167
- logger_1.logger.error('Capillary API returned errors', { brand, errors: data.errors });
285
+ logger_1.logger.error("Capillary API returned errors", {
286
+ brand,
287
+ errors: data.errors,
288
+ });
168
289
  return [];
169
290
  }
170
291
  // v2 response format: entity.customers[0].coupons[]
@@ -173,8 +294,10 @@ let CapillaryCouponService = class CapillaryCouponService {
173
294
  couponCode: c.code,
174
295
  seriesId: c.seriesId,
175
296
  redemptionId: c.redemptions?.[0]?.id,
176
- status: (c.redemptionCount > 0 ? 'REDEEMED' : 'ACTIVE'),
177
- issuedAt: c.createdDate ?? '',
297
+ status: (c.redemptionCount > 0
298
+ ? "REDEEMED"
299
+ : "ACTIVE"),
300
+ issuedAt: c.createdDate ?? "",
178
301
  validTill: c.validTill,
179
302
  discountType: c.discountType,
180
303
  discountValue: c.discountValue,
@@ -182,10 +305,10 @@ let CapillaryCouponService = class CapillaryCouponService {
182
305
  }));
183
306
  }
184
307
  catch (error) {
185
- logger_1.logger.error('Failed to get coupons by transaction', {
308
+ logger_1.logger.error("Failed to get coupons by transaction", {
186
309
  brand,
187
310
  transactionNumber,
188
- error: error instanceof Error ? error.message : 'Unknown',
311
+ error: error instanceof Error ? error.message : "Unknown",
189
312
  });
190
313
  return [];
191
314
  }
@@ -198,16 +321,20 @@ let CapillaryCouponService = class CapillaryCouponService {
198
321
  const token = await this.authService.getValidAccessToken(brand);
199
322
  const url = `${config.baseUrl}/v2/coupon/reactivate`;
200
323
  const body = { redemptionIds };
201
- logger_1.logger.info('Reactivating coupons', { brand, redemptionIds });
324
+ logger_1.logger.info("Reactivating coupons", { brand, redemptionIds });
202
325
  try {
203
326
  const response = await this.fetchWithTimeout(url, {
204
- method: 'POST',
327
+ method: "POST",
205
328
  headers: this.getHeaders(token, config),
206
329
  body: JSON.stringify(body),
207
330
  });
208
331
  if (!response.ok) {
209
332
  const errorText = await response.text();
210
- logger_1.logger.error('Capillary Reactivate API error', { brand, status: response.status, error: errorText });
333
+ logger_1.logger.error("Capillary Reactivate API error", {
334
+ brand,
335
+ status: response.status,
336
+ error: errorText,
337
+ });
211
338
  return {
212
339
  success: false,
213
340
  reactivatedCount: 0,
@@ -217,23 +344,23 @@ let CapillaryCouponService = class CapillaryCouponService {
217
344
  }
218
345
  const data = (await response.json());
219
346
  const success = data.response?.status?.success ?? false;
220
- logger_1.logger.info('Coupons reactivated', { brand, redemptionIds, success });
347
+ logger_1.logger.info("Coupons reactivated", { brand, redemptionIds, success });
221
348
  return {
222
349
  success,
223
350
  reactivatedCount: success ? redemptionIds.length : 0,
224
351
  };
225
352
  }
226
353
  catch (error) {
227
- logger_1.logger.error('Coupon reactivation failed', {
354
+ logger_1.logger.error("Coupon reactivation failed", {
228
355
  brand,
229
356
  redemptionIds,
230
- error: error instanceof Error ? error.message : 'Unknown',
357
+ error: error instanceof Error ? error.message : "Unknown",
231
358
  });
232
359
  return {
233
360
  success: false,
234
361
  reactivatedCount: 0,
235
362
  errorCode: 500,
236
- errorMessage: error instanceof Error ? error.message : 'Reactivation failed',
363
+ errorMessage: error instanceof Error ? error.message : "Reactivation failed",
237
364
  };
238
365
  }
239
366
  }
@@ -245,16 +372,20 @@ let CapillaryCouponService = class CapillaryCouponService {
245
372
  const token = await this.authService.getValidAccessToken(brand);
246
373
  const url = `${config.baseUrl}/v2/coupon/revoke`;
247
374
  const body = { couponCodes };
248
- logger_1.logger.info('Revoking coupons', { brand, couponCodes });
375
+ logger_1.logger.info("Revoking coupons", { brand, couponCodes });
249
376
  try {
250
377
  const response = await this.fetchWithTimeout(url, {
251
- method: 'POST',
378
+ method: "POST",
252
379
  headers: this.getHeaders(token, config),
253
380
  body: JSON.stringify(body),
254
381
  });
255
382
  if (!response.ok) {
256
383
  const errorText = await response.text();
257
- logger_1.logger.error('Capillary Revoke API error', { brand, status: response.status, error: errorText });
384
+ logger_1.logger.error("Capillary Revoke API error", {
385
+ brand,
386
+ status: response.status,
387
+ error: errorText,
388
+ });
258
389
  return {
259
390
  success: false,
260
391
  revokedCount: 0,
@@ -264,23 +395,23 @@ let CapillaryCouponService = class CapillaryCouponService {
264
395
  }
265
396
  const data = (await response.json());
266
397
  const success = data.response?.status?.success ?? false;
267
- logger_1.logger.info('Coupons revoked', { brand, couponCodes, success });
398
+ logger_1.logger.info("Coupons revoked", { brand, couponCodes, success });
268
399
  return {
269
400
  success,
270
401
  revokedCount: success ? couponCodes.length : 0,
271
402
  };
272
403
  }
273
404
  catch (error) {
274
- logger_1.logger.error('Coupon revocation failed', {
405
+ logger_1.logger.error("Coupon revocation failed", {
275
406
  brand,
276
407
  couponCodes,
277
- error: error instanceof Error ? error.message : 'Unknown',
408
+ error: error instanceof Error ? error.message : "Unknown",
278
409
  });
279
410
  return {
280
411
  success: false,
281
412
  revokedCount: 0,
282
413
  errorCode: 500,
283
- errorMessage: error instanceof Error ? error.message : 'Revocation failed',
414
+ errorMessage: error instanceof Error ? error.message : "Revocation failed",
284
415
  };
285
416
  }
286
417
  }
@@ -292,26 +423,34 @@ let CapillaryCouponService = class CapillaryCouponService {
292
423
  const config = this.authService.getCapillaryConfig(brand);
293
424
  const token = await this.authService.getValidAccessToken(brand);
294
425
  const url = `${config.baseUrl}/v2/transactions/getByBillNumber/${encodeURIComponent(billNumber)}`;
295
- logger_1.logger.info('Getting transaction ID by bill number', { brand, billNumber });
426
+ logger_1.logger.info("Getting transaction ID by bill number", { brand, billNumber });
296
427
  try {
297
428
  const response = await this.fetchWithTimeout(url, {
298
- method: 'GET',
429
+ method: "GET",
299
430
  headers: this.getHeaders(token, config),
300
431
  });
301
432
  if (!response.ok) {
302
- logger_1.logger.error('Capillary GetByBillNumber API error', { brand, billNumber, status: response.status });
433
+ logger_1.logger.error("Capillary GetByBillNumber API error", {
434
+ brand,
435
+ billNumber,
436
+ status: response.status,
437
+ });
303
438
  return null;
304
439
  }
305
440
  const data = (await response.json());
306
441
  const transactionId = data.response?.transactions?.transaction?.[0]?.id ?? null;
307
- logger_1.logger.info('Transaction ID retrieved', { brand, billNumber, transactionId });
442
+ logger_1.logger.info("Transaction ID retrieved", {
443
+ brand,
444
+ billNumber,
445
+ transactionId,
446
+ });
308
447
  return transactionId;
309
448
  }
310
449
  catch (error) {
311
- logger_1.logger.error('Failed to get transaction ID by bill number', {
450
+ logger_1.logger.error("Failed to get transaction ID by bill number", {
312
451
  brand,
313
452
  billNumber,
314
- error: error instanceof Error ? error.message : 'Unknown',
453
+ error: error instanceof Error ? error.message : "Unknown",
315
454
  });
316
455
  return null;
317
456
  }
@@ -327,21 +466,31 @@ let CapillaryCouponService = class CapillaryCouponService {
327
466
  const token = await this.authService.getValidAccessToken(brand);
328
467
  // Use mobile param directly - identifierName/identifierValue returns 400 error
329
468
  const url = new URL(`${config.baseUrl}/v2/customers/coupons`);
330
- url.searchParams.set('mobile', mobile);
331
- logger_1.logger.info('Getting coupons earned on transaction', { brand, mobile: mobile.slice(-4), transactionId });
469
+ url.searchParams.set("mobile", mobile);
470
+ logger_1.logger.info("Getting coupons earned on transaction", {
471
+ brand,
472
+ mobile: mobile.slice(-4),
473
+ transactionId,
474
+ });
332
475
  try {
333
476
  const response = await this.fetchWithTimeout(url.toString(), {
334
- method: 'GET',
477
+ method: "GET",
335
478
  headers: this.getHeaders(token, config),
336
479
  });
337
480
  if (!response.ok) {
338
- logger_1.logger.error('Capillary Get Coupons API error', { brand, status: response.status });
481
+ logger_1.logger.error("Capillary Get Coupons API error", {
482
+ brand,
483
+ status: response.status,
484
+ });
339
485
  return [];
340
486
  }
341
487
  const data = (await response.json());
342
488
  // Check for API-level errors
343
489
  if (data.errors && data.errors.length > 0) {
344
- logger_1.logger.error('Capillary API returned errors', { brand, errors: data.errors });
490
+ logger_1.logger.error("Capillary API returned errors", {
491
+ brand,
492
+ errors: data.errors,
493
+ });
345
494
  return [];
346
495
  }
347
496
  // v2 response format: entity.customers[0].coupons[]
@@ -354,7 +503,7 @@ let CapillaryCouponService = class CapillaryCouponService {
354
503
  couponCode: c.code,
355
504
  seriesId: c.seriesId,
356
505
  }));
357
- logger_1.logger.info('Coupons earned on transaction retrieved', {
506
+ logger_1.logger.info("Coupons earned on transaction retrieved", {
358
507
  brand,
359
508
  transactionId,
360
509
  couponCount: earnedCoupons.length,
@@ -362,10 +511,10 @@ let CapillaryCouponService = class CapillaryCouponService {
362
511
  return earnedCoupons;
363
512
  }
364
513
  catch (error) {
365
- logger_1.logger.error('Failed to get coupons earned on transaction', {
514
+ logger_1.logger.error("Failed to get coupons earned on transaction", {
366
515
  brand,
367
516
  transactionId,
368
- error: error instanceof Error ? error.message : 'Unknown',
517
+ error: error instanceof Error ? error.message : "Unknown",
369
518
  });
370
519
  return [];
371
520
  }
@@ -381,21 +530,31 @@ let CapillaryCouponService = class CapillaryCouponService {
381
530
  const token = await this.authService.getValidAccessToken(brand);
382
531
  // Use mobile param directly - identifierName/identifierValue returns 400 error
383
532
  const url = new URL(`${config.baseUrl}/v2/customers/coupons`);
384
- url.searchParams.set('mobile', mobile);
385
- logger_1.logger.info('Getting redemption ID for transaction', { brand, mobile: mobile.slice(-4), transactionNumber });
533
+ url.searchParams.set("mobile", mobile);
534
+ logger_1.logger.info("Getting redemption ID for transaction", {
535
+ brand,
536
+ mobile: mobile.slice(-4),
537
+ transactionNumber,
538
+ });
386
539
  try {
387
540
  const response = await this.fetchWithTimeout(url.toString(), {
388
- method: 'GET',
541
+ method: "GET",
389
542
  headers: this.getHeaders(token, config),
390
543
  });
391
544
  if (!response.ok) {
392
- logger_1.logger.error('Capillary Get Coupons API error', { brand, status: response.status });
545
+ logger_1.logger.error("Capillary Get Coupons API error", {
546
+ brand,
547
+ status: response.status,
548
+ });
393
549
  return null;
394
550
  }
395
551
  const data = (await response.json());
396
552
  // Check for API-level errors
397
553
  if (data.errors && data.errors.length > 0) {
398
- logger_1.logger.error('Capillary API returned errors', { brand, errors: data.errors });
554
+ logger_1.logger.error("Capillary API returned errors", {
555
+ brand,
556
+ errors: data.errors,
557
+ });
399
558
  return null;
400
559
  }
401
560
  // v2 response format: entity.customers[0].coupons[]
@@ -406,7 +565,7 @@ let CapillaryCouponService = class CapillaryCouponService {
406
565
  // Check if any redemption matches this transaction number
407
566
  for (const redemption of coupon.redemptions) {
408
567
  if (redemption.transactionNumber === transactionNumber) {
409
- logger_1.logger.info('Redemption ID found for transaction', {
568
+ logger_1.logger.info("Redemption ID found for transaction", {
410
569
  brand,
411
570
  transactionNumber,
412
571
  redemptionId: redemption.id,
@@ -417,67 +576,159 @@ let CapillaryCouponService = class CapillaryCouponService {
417
576
  }
418
577
  }
419
578
  }
420
- logger_1.logger.info('No redemption ID found for transaction', { brand, transactionNumber });
579
+ logger_1.logger.info("No redemption ID found for transaction", {
580
+ brand,
581
+ transactionNumber,
582
+ });
421
583
  return null;
422
584
  }
423
585
  catch (error) {
424
- logger_1.logger.error('Failed to get redemption ID for transaction', {
586
+ logger_1.logger.error("Failed to get redemption ID for transaction", {
425
587
  brand,
426
588
  transactionNumber,
427
- error: error instanceof Error ? error.message : 'Unknown',
589
+ error: error instanceof Error ? error.message : "Unknown",
428
590
  });
429
591
  return null;
430
592
  }
431
593
  }
432
- parseValidationResponse(data, originalCode, returnPolicyDays) {
594
+ /**
595
+ * List a customer's coupons by mobile number.
596
+ *
597
+ * Only CouponState.ACTIVE is implemented. REDEEMABLE and BOTH return empty lists with a message.
598
+ *
599
+ * Uses v2/customers/coupons API with mobile param (not identifierName/identifierValue)
600
+ */
601
+ async getCustomerCoupons(brand, mobile, state) {
602
+ switch (state) {
603
+ case capillary_types_1.CouponState.ACTIVE:
604
+ return this.getActiveCoupons(brand, mobile);
605
+ case capillary_types_1.CouponState.REDEEMABLE:
606
+ case capillary_types_1.CouponState.BOTH:
607
+ logger_1.logger.info("Coupon state not implemented yet", { brand, state });
608
+ return {
609
+ coupons: [],
610
+ message: `Coupon state ${state} is not supported yet. Only ACTIVE is available.`,
611
+ };
612
+ default:
613
+ logger_1.logger.warn("Unknown coupon state requested", { brand, state });
614
+ return {
615
+ coupons: [],
616
+ message: `Unknown coupon state: ${state}`,
617
+ };
618
+ }
619
+ }
620
+ /**
621
+ * Fetch every coupon Capillary reports as ACTIVE for this mobile number.
622
+ *
623
+ * The listing endpoint is paginated. `pagination.total` reflects the page
624
+ * rather than the full result set, so it cannot be used to know when to stop.
625
+ * Paging continues until a page comes back shorter than the requested limit.
626
+ */
627
+ async getActiveCoupons(brand, mobile) {
628
+ const config = this.authService.getCapillaryConfig(brand);
629
+ const token = await this.authService.getValidAccessToken(brand);
630
+ logger_1.logger.info("Getting active coupons for customer", { brand, mobile });
631
+ const coupons = [];
632
+ let offset = 0;
633
+ while (true) {
634
+ // Use mobile param directly - identifierName/identifierValue returns 400 error
635
+ const url = new URL(`${config.baseUrl}/v2/customers/coupons`);
636
+ url.searchParams.set("mobile", mobile);
637
+ url.searchParams.set("status", capillary_types_1.CAPILLARY_STATUS_ACTIVE);
638
+ url.searchParams.set("limit", this.COUPON_PAGE_SIZE.toString());
639
+ url.searchParams.set("offset", offset.toString());
640
+ const response = await this.fetchWithTimeout(url.toString(), {
641
+ method: "GET",
642
+ headers: this.getHeaders(token, config),
643
+ });
644
+ if (!response.ok) {
645
+ logger_1.logger.error("Capillary Get Coupons API error", {
646
+ brand,
647
+ status: response.status,
648
+ offset,
649
+ });
650
+ throw new Error(`Capillary coupon listing failed with status ${response.status}`);
651
+ }
652
+ const data = (await response.json());
653
+ // Capillary answers 200 even when the lookup failed, so the body decides.
654
+ if (data.errors && data.errors.length > 0) {
655
+ // An unregistered mobile is not a failure, it is an empty result. It
656
+ // only ever comes back on the first page, because the customer either
657
+ // exists or does not.
658
+ const notFound = data.errors.some((e) => e.code === capillary_types_1.CAPILLARY_ERROR_CODES.CUSTOMER_NOT_FOUND);
659
+ if (notFound) {
660
+ logger_1.logger.info("No Capillary customer for mobile", { brand, mobile });
661
+ return { coupons: [], message: capillary_types_1.CUSTOMER_NOT_FOUND_MESSAGE };
662
+ }
663
+ // Anything else is a real failure and must surface, so an outage is
664
+ // never flattened into an empty coupon list.
665
+ logger_1.logger.error("Capillary API returned errors", {
666
+ brand,
667
+ errors: data.errors,
668
+ });
669
+ throw new Error(data.errors[0]?.message ?? "Capillary coupon listing failed");
670
+ }
671
+ // The v2 listing nests coupons under the matched customer. We look up by a
672
+ // single mobile number, so Capillary returns at most one customer, and the
673
+ // coupons live at entity.customers[0].coupons.
674
+ const customer = data.entity?.customers?.[0];
675
+ const pageCoupons = customer?.coupons ?? [];
676
+ coupons.push(...pageCoupons.map((c) => ({
677
+ code: c.code,
678
+ seriesId: c.seriesId,
679
+ description: c.description,
680
+ validTill: c.validTill,
681
+ validTillDateTime: c.validTillDateTime,
682
+ discountType: c.discountType,
683
+ discountValue: c.discountValue,
684
+ discountUpto: c.discountUpto,
685
+ redemptionsLeft: c.redemptionsLeft,
686
+ issuedOn: c.createdDate,
687
+ })));
688
+ // A short page means there is nothing left to fetch
689
+ if (pageCoupons.length < this.COUPON_PAGE_SIZE) {
690
+ break;
691
+ }
692
+ offset += this.COUPON_PAGE_SIZE;
693
+ }
694
+ logger_1.logger.info("Active coupons retrieved", {
695
+ brand,
696
+ mobile,
697
+ couponCount: coupons.length,
698
+ });
699
+ return {
700
+ coupons,
701
+ message: coupons.length > 0
702
+ ? `${coupons.length} active coupon(s) found`
703
+ : "No active coupons found for this customer",
704
+ };
705
+ }
706
+ parseValidationResponse(data, originalCode) {
433
707
  const coupon = data.response?.coupons?.redeemable;
434
708
  if (!coupon) {
435
709
  return {
436
710
  valid: false,
437
711
  couponCode: originalCode,
438
- errorCode: 'NOT_FOUND',
439
- errorMessage: 'Coupon not found',
712
+ errorCode: "NOT_FOUND",
713
+ errorMessage: "Coupon not found",
440
714
  };
441
715
  }
442
- const isRedeemable = coupon.is_redeemable === 'true';
716
+ const isRedeemable = coupon.is_redeemable === "true";
443
717
  if (!isRedeemable) {
444
718
  const itemStatus = coupon.item_status;
445
719
  return {
446
720
  valid: false,
447
721
  couponCode: originalCode,
448
- errorCode: itemStatus?.code?.toString() ?? 'INVALID',
449
- errorMessage: itemStatus?.message ?? 'Coupon is not redeemable',
722
+ errorCode: itemStatus?.code?.toString() ?? "INVALID",
723
+ errorMessage: itemStatus?.message ?? "Coupon is not redeemable",
450
724
  };
451
725
  }
452
- // Check cool-off period
453
- const createdDate = coupon.series_info?.created;
454
- if (createdDate) {
455
- const issuedAt = new Date(createdDate);
456
- const usableFromDate = new Date(issuedAt);
457
- usableFromDate.setDate(usableFromDate.getDate() + returnPolicyDays);
458
- const now = new Date();
459
- if (now < usableFromDate) {
460
- const formattedDate = usableFromDate.toLocaleDateString('en-IN', {
461
- year: 'numeric',
462
- month: 'long',
463
- day: 'numeric',
464
- });
465
- return {
466
- valid: false,
467
- couponCode: originalCode,
468
- issuedAt: createdDate,
469
- usableFromDate: usableFromDate.toISOString(),
470
- errorCode: capillary_types_1.COUPON_COOLOFF_ERROR.errorCode,
471
- errorMessage: capillary_types_1.COUPON_COOLOFF_ERROR.errorMessage(formattedDate),
472
- };
473
- }
474
- }
475
726
  const seriesInfo = coupon.series_info;
476
727
  return {
477
728
  valid: true,
478
729
  couponCode: originalCode,
479
- discountType: (seriesInfo?.discount_type ?? 'ABS'),
480
- discountValue: seriesInfo?.discount_value ?? parseFloat(coupon.coupon_value ?? '0'),
730
+ discountType: (seriesInfo?.discount_type ?? "ABS"),
731
+ discountValue: seriesInfo?.discount_value ?? parseFloat(coupon.coupon_value ?? "0"),
481
732
  discountUpto: seriesInfo?.discount_upto,
482
733
  minBillAmount: seriesInfo?.min_bill_amount,
483
734
  validTill: seriesInfo?.valid_till_date,
@@ -488,10 +739,10 @@ let CapillaryCouponService = class CapillaryCouponService {
488
739
  }
489
740
  getHeaders(token, config) {
490
741
  return {
491
- 'Content-Type': 'application/json',
492
- 'X-CAP-API-OAUTH-TOKEN': token,
493
- 'X-CAP-API-ATTRIBUTION-ENTITY-TYPE': 'TILL',
494
- 'X-CAP-API-ATTRIBUTION-ENTITY-CODE': config.tillId,
742
+ "Content-Type": "application/json",
743
+ "X-CAP-API-OAUTH-TOKEN": token,
744
+ "X-CAP-API-ATTRIBUTION-ENTITY-TYPE": "TILL",
745
+ "X-CAP-API-ATTRIBUTION-ENTITY-CODE": config.tillId,
495
746
  };
496
747
  }
497
748
  async fetchWithTimeout(url, options) {