@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.
- package/dist/integrations/commerce/shopify-admin.service.d.ts.map +1 -1
- package/dist/integrations/commerce/shopify-admin.service.js +3 -1
- package/dist/integrations/commerce/shopify-admin.service.js.map +1 -1
- package/dist/integrations/loyalty/capillary-coupon.service.d.ts +24 -2
- package/dist/integrations/loyalty/capillary-coupon.service.d.ts.map +1 -1
- package/dist/integrations/loyalty/capillary-coupon.service.js +359 -108
- package/dist/integrations/loyalty/capillary-coupon.service.js.map +1 -1
- package/dist/integrations/loyalty/capillary.types.d.ts +104 -5
- package/dist/integrations/loyalty/capillary.types.d.ts.map +1 -1
- package/dist/integrations/loyalty/capillary.types.js +37 -5
- package/dist/integrations/loyalty/capillary.types.js.map +1 -1
- package/package.json +1 -1
|
@@ -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(
|
|
25
|
-
url.searchParams.set(
|
|
26
|
-
url.searchParams.set(
|
|
27
|
-
logger_1.logger.info(
|
|
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:
|
|
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(
|
|
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:
|
|
48
|
+
errorCode: "API_ERROR",
|
|
40
49
|
errorMessage: `Capillary API error: ${response.status}`,
|
|
41
50
|
};
|
|
42
51
|
}
|
|
43
52
|
const data = (await response.json());
|
|
44
|
-
|
|
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(
|
|
108
|
+
logger_1.logger.error("Capillary coupon validation failed", {
|
|
48
109
|
brand,
|
|
49
110
|
couponCode,
|
|
50
|
-
error: error instanceof Error ? error.message :
|
|
111
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
51
112
|
});
|
|
52
113
|
return {
|
|
53
114
|
valid: false,
|
|
54
115
|
couponCode,
|
|
55
|
-
errorCode:
|
|
56
|
-
errorMessage: error instanceof Error ? error.message :
|
|
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(
|
|
65
|
-
url.searchParams.set(
|
|
66
|
-
url.searchParams.set(
|
|
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 ??
|
|
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) => ({
|
|
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 ??
|
|
193
|
+
type: item.type ?? "REGULAR",
|
|
85
194
|
})),
|
|
86
195
|
paymentModes: request.paymentModes,
|
|
87
196
|
};
|
|
88
|
-
logger_1.logger.info(
|
|
197
|
+
logger_1.logger.info("Creating Capillary transaction", {
|
|
89
198
|
brand,
|
|
90
199
|
billNumber: request.billNumber,
|
|
91
|
-
type: request.type ??
|
|
200
|
+
type: request.type ?? "REGULAR",
|
|
92
201
|
});
|
|
93
202
|
try {
|
|
94
203
|
const response = await this.fetchWithTimeout(url.toString(), {
|
|
95
|
-
method:
|
|
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(
|
|
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:
|
|
229
|
+
errorMessage: "No transaction in response",
|
|
117
230
|
};
|
|
118
231
|
}
|
|
119
|
-
logger_1.logger.info(
|
|
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(
|
|
244
|
+
logger_1.logger.error("Capillary transaction creation failed", {
|
|
132
245
|
brand,
|
|
133
246
|
billNumber: request.billNumber,
|
|
134
|
-
error: error instanceof Error ? error.message :
|
|
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
|
|
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(
|
|
154
|
-
logger_1.logger.info(
|
|
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:
|
|
272
|
+
method: "GET",
|
|
158
273
|
headers: this.getHeaders(token, config),
|
|
159
274
|
});
|
|
160
275
|
if (!response.ok) {
|
|
161
|
-
logger_1.logger.error(
|
|
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(
|
|
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
|
|
177
|
-
|
|
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(
|
|
308
|
+
logger_1.logger.error("Failed to get coupons by transaction", {
|
|
186
309
|
brand,
|
|
187
310
|
transactionNumber,
|
|
188
|
-
error: error instanceof Error ? error.message :
|
|
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(
|
|
324
|
+
logger_1.logger.info("Reactivating coupons", { brand, redemptionIds });
|
|
202
325
|
try {
|
|
203
326
|
const response = await this.fetchWithTimeout(url, {
|
|
204
|
-
method:
|
|
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(
|
|
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(
|
|
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(
|
|
354
|
+
logger_1.logger.error("Coupon reactivation failed", {
|
|
228
355
|
brand,
|
|
229
356
|
redemptionIds,
|
|
230
|
-
error: error instanceof Error ? error.message :
|
|
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 :
|
|
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(
|
|
375
|
+
logger_1.logger.info("Revoking coupons", { brand, couponCodes });
|
|
249
376
|
try {
|
|
250
377
|
const response = await this.fetchWithTimeout(url, {
|
|
251
|
-
method:
|
|
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(
|
|
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(
|
|
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(
|
|
405
|
+
logger_1.logger.error("Coupon revocation failed", {
|
|
275
406
|
brand,
|
|
276
407
|
couponCodes,
|
|
277
|
-
error: error instanceof Error ? error.message :
|
|
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 :
|
|
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(
|
|
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:
|
|
429
|
+
method: "GET",
|
|
299
430
|
headers: this.getHeaders(token, config),
|
|
300
431
|
});
|
|
301
432
|
if (!response.ok) {
|
|
302
|
-
logger_1.logger.error(
|
|
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(
|
|
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(
|
|
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 :
|
|
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(
|
|
331
|
-
logger_1.logger.info(
|
|
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:
|
|
477
|
+
method: "GET",
|
|
335
478
|
headers: this.getHeaders(token, config),
|
|
336
479
|
});
|
|
337
480
|
if (!response.ok) {
|
|
338
|
-
logger_1.logger.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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 :
|
|
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(
|
|
385
|
-
logger_1.logger.info(
|
|
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:
|
|
541
|
+
method: "GET",
|
|
389
542
|
headers: this.getHeaders(token, config),
|
|
390
543
|
});
|
|
391
544
|
if (!response.ok) {
|
|
392
|
-
logger_1.logger.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 :
|
|
589
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
428
590
|
});
|
|
429
591
|
return null;
|
|
430
592
|
}
|
|
431
593
|
}
|
|
432
|
-
|
|
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:
|
|
439
|
-
errorMessage:
|
|
712
|
+
errorCode: "NOT_FOUND",
|
|
713
|
+
errorMessage: "Coupon not found",
|
|
440
714
|
};
|
|
441
715
|
}
|
|
442
|
-
const isRedeemable = coupon.is_redeemable ===
|
|
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() ??
|
|
449
|
-
errorMessage: itemStatus?.message ??
|
|
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 ??
|
|
480
|
-
discountValue: seriesInfo?.discount_value ?? parseFloat(coupon.coupon_value ??
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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) {
|