@unloq/unloq-code-validtor 1.0.0

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,151 @@
1
+ import { validateUnloqDiscountCode } from '../index';
2
+ import { Reason } from '../interface/reason';
3
+ import { getSHA256Hash } from '../utils/hash';
4
+ describe('validateGiftCardCode', () => {
5
+ const tests = [
6
+ {
7
+ name: "invalid fealtyx code, first 2 of last 4 characters don't match last 2",
8
+ domain: "example.com",
9
+ phoneNumber: "+918989898989",
10
+ giftCardCode: "FLX1234ABCD",
11
+ orderAmount: 100.0,
12
+ is_applicable: true,
13
+ is_unloq_discount_code: false,
14
+ reason: null
15
+ },
16
+ {
17
+ name: "invalid fealtyx code - missing prefix",
18
+ domain: "example.com",
19
+ phoneNumber: "+918989898989",
20
+ giftCardCode: "123415EG",
21
+ orderAmount: 100.0,
22
+ is_applicable: true,
23
+ is_unloq_discount_code: false,
24
+ reason: null
25
+ },
26
+ {
27
+ name: "invalid fealtyx code - short code",
28
+ domain: "example.com",
29
+ phoneNumber: "+918989898989",
30
+ giftCardCode: "FL1",
31
+ orderAmount: 100.0,
32
+ is_applicable: true,
33
+ is_unloq_discount_code: false,
34
+ reason: null
35
+ },
36
+ {
37
+ name: "valid fealtyx code - domain mismatch",
38
+ domain: "example2.com",
39
+ phoneNumber: "+918989898989",
40
+ giftCardCode: "FLX123415EG",
41
+ orderAmount: 100.0,
42
+ is_applicable: false,
43
+ is_unloq_discount_code: true,
44
+ reason: Reason.VoucherNotEligible
45
+ },
46
+ {
47
+ name: "valid fealtyx code - phone mismatch",
48
+ domain: "example.com",
49
+ phoneNumber: "777777",
50
+ giftCardCode: "FLX123415EG",
51
+ orderAmount: 100.0,
52
+ is_applicable: false,
53
+ is_unloq_discount_code: true,
54
+ reason: Reason.VoucherNotEligible
55
+ },
56
+ {
57
+ name: "valid fealtyx code with FLX prefix",
58
+ domain: "example.com",
59
+ phoneNumber: "+918989898989",
60
+ giftCardCode: "FLX123415EG",
61
+ orderAmount: 100.0,
62
+ is_applicable: true,
63
+ is_unloq_discount_code: true,
64
+ reason: null
65
+ },
66
+ {
67
+ name: "valid fealtyx code with UNQ prefix",
68
+ domain: "example.com",
69
+ phoneNumber: "+918989898989",
70
+ giftCardCode: "UNQ123415EG",
71
+ orderAmount: 100.0,
72
+ is_applicable: true,
73
+ is_unloq_discount_code: true,
74
+ reason: null
75
+ },
76
+ {
77
+ name: "valid fealtyx code with hashed phone",
78
+ domain: "example.com",
79
+ phoneNumber: getSHA256Hash("+918989898989"),
80
+ giftCardCode: "FLX123415EG",
81
+ orderAmount: 100.0,
82
+ is_applicable: true,
83
+ is_unloq_discount_code: true,
84
+ reason: null
85
+ },
86
+ {
87
+ name: "valid fealtyx code with non hashed phone",
88
+ domain: "example.com",
89
+ phoneNumber: "+918989898989",
90
+ giftCardCode: "FLX123415EG",
91
+ orderAmount: 100.0,
92
+ is_applicable: true,
93
+ is_unloq_discount_code: true,
94
+ reason: null
95
+ },
96
+ {
97
+ name: "valid fealtyx code with hashed phone but no order amount",
98
+ domain: "example.com",
99
+ phoneNumber: getSHA256Hash("+918989898989"),
100
+ giftCardCode: "FLX123415EG",
101
+ orderAmount: 0,
102
+ is_applicable: true,
103
+ is_unloq_discount_code: true,
104
+ reason: null
105
+ },
106
+ {
107
+ name: "valid fealtyx code with hashed phone but empty domain",
108
+ domain: "",
109
+ phoneNumber: getSHA256Hash("+918989898989"),
110
+ giftCardCode: "FLX123415EG",
111
+ orderAmount: 100.0,
112
+ is_applicable: false,
113
+ is_unloq_discount_code: true,
114
+ reason: Reason.InvalidDomain
115
+ },
116
+ {
117
+ name: "valid fealtyx code with hashed phone but invalid domain",
118
+ domain: "example\x00.com",
119
+ phoneNumber: getSHA256Hash("+918989898989"),
120
+ giftCardCode: "FLX123415EG",
121
+ orderAmount: 100.0,
122
+ is_applicable: false,
123
+ is_unloq_discount_code: true,
124
+ reason: Reason.InvalidDomain
125
+ },
126
+ {
127
+ name: "non-fealtyx code",
128
+ domain: "example.com",
129
+ phoneNumber: "+918989898989",
130
+ giftCardCode: "ABCD1234EFGH",
131
+ orderAmount: 100.0,
132
+ is_applicable: true,
133
+ is_unloq_discount_code: false,
134
+ reason: null
135
+ },
136
+ ];
137
+ tests.forEach(testCase => {
138
+ it(testCase.name, () => {
139
+ const result = validateUnloqDiscountCode(testCase.domain, testCase.phoneNumber, testCase.giftCardCode, testCase.orderAmount);
140
+ expect(result.is_applicable).toBe(testCase.is_applicable);
141
+ expect(result.is_unloq_discount_code).toBe(testCase.is_unloq_discount_code);
142
+ // Handle both string and Reason enum comparison
143
+ if (testCase.reason === null) {
144
+ expect(result.reason).toBeNull();
145
+ }
146
+ else {
147
+ expect(result.reason).toBe(testCase.reason);
148
+ }
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,7 @@
1
+ import { Reason } from "./interface/reason";
2
+ export type ValidationResult = {
3
+ is_applicable: boolean;
4
+ is_unloq_discount_code: boolean;
5
+ reason: Reason | null;
6
+ };
7
+ export declare function validateUnloqDiscountCode(domain: string, phoneNumber: string, giftCardCode: string, orderAmount: number): ValidationResult;
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ import { Reason } from "./interface/reason";
2
+ import { getSHA256Hash, getPhoneNumberHash } from "./utils/hash";
3
+ import { hexCharToByte } from "./utils/string";
4
+ export function validateUnloqDiscountCode(domain, phoneNumber, giftCardCode, orderAmount) {
5
+ // First, clean the gift card code
6
+ giftCardCode = giftCardCode.toLowerCase().trim();
7
+ // If gift card code is too short, return
8
+ if (!giftCardCode || giftCardCode.length < 4) {
9
+ return {
10
+ is_applicable: true,
11
+ is_unloq_discount_code: false,
12
+ reason: null,
13
+ };
14
+ }
15
+ // Check if gift card code starts with "flx" or "unq"
16
+ if (!giftCardCode.startsWith("flx") && !giftCardCode.startsWith("unq")) {
17
+ return {
18
+ is_applicable: true,
19
+ is_unloq_discount_code: false,
20
+ reason: null,
21
+ };
22
+ }
23
+ // Get last 4 characters of the code
24
+ const last4 = giftCardCode.slice(-4);
25
+ if (last4.length !== 4) {
26
+ return {
27
+ is_applicable: true,
28
+ is_unloq_discount_code: false,
29
+ reason: null,
30
+ };
31
+ }
32
+ const first2 = last4.slice(0, 2);
33
+ const actualLast2 = last4.slice(2);
34
+ // Step 1: Check if this is an Fealtyx gift card by checking if the last 2 characters match the expected last 2 characters
35
+ const expectedLast2 = generateLast2FromFirst2(first2);
36
+ if (actualLast2 !== expectedLast2) {
37
+ // If the last 2 don't match the expected pattern, the gift card is not a Fealtyx gift card
38
+ return {
39
+ is_applicable: true,
40
+ is_unloq_discount_code: false,
41
+ reason: null,
42
+ };
43
+ }
44
+ // Sanitize domain
45
+ const { result: sanitizedDomain, error: err } = sanitizeDomain(domain);
46
+ if (err !== null) {
47
+ return {
48
+ is_applicable: false,
49
+ is_unloq_discount_code: true,
50
+ reason: Reason.InvalidDomain,
51
+ };
52
+ }
53
+ // Step 2: Verify full code - generate expected suffix
54
+ const expectedSuffix = getGiftCardCodeIdentifier(sanitizedDomain, phoneNumber).toLowerCase();
55
+ // Case-insensitive match
56
+ const valid = expectedSuffix.toLowerCase() === last4.toLowerCase();
57
+ if (!valid) {
58
+ return {
59
+ is_applicable: false,
60
+ is_unloq_discount_code: true,
61
+ reason: Reason.VoucherNotEligible,
62
+ };
63
+ }
64
+ return {
65
+ is_applicable: true,
66
+ is_unloq_discount_code: true,
67
+ reason: null,
68
+ };
69
+ }
70
+ // Helper function to generate last 2 characters from first 2 characters
71
+ function generateLast2FromFirst2(first2) {
72
+ const alphanum = "abcdefghijklmnopqrstuvwxyz0123456789";
73
+ const charsetLen = alphanum.length;
74
+ if (first2.length !== 2) {
75
+ return "";
76
+ }
77
+ // Convert hex characters to numeric byte values
78
+ const c1 = first2[0];
79
+ const c2 = first2[1];
80
+ const b1 = hexCharToByte(c1);
81
+ const b2 = hexCharToByte(c2);
82
+ // Get XOR and sum of first 2 characters
83
+ const xor = b1 ^ b2;
84
+ const sum = b1 + b2;
85
+ // Map XOR and sum results to character set
86
+ const idx1 = xor % charsetLen;
87
+ const idx2 = sum % charsetLen;
88
+ return alphanum[idx1] + alphanum[idx2];
89
+ }
90
+ function getGiftCardCodeIdentifier(partnerEntityId, phoneNumber) {
91
+ let hashedPhoneNumber;
92
+ hashedPhoneNumber = getPhoneNumberHash(phoneNumber);
93
+ const alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
94
+ const charsetLen = alphanum.length;
95
+ let input = (partnerEntityId + hashedPhoneNumber).toLowerCase();
96
+ const hash = getSHA256Hash(input);
97
+ const c1 = hash[0];
98
+ const c2 = hash[1];
99
+ const b1 = hexCharToByte(c1);
100
+ const b2 = hexCharToByte(c2);
101
+ const xor = b1 ^ b2;
102
+ const idx1 = xor % charsetLen;
103
+ const idx2 = (b1 + b2) % charsetLen;
104
+ return `${c1}${c2}${alphanum[idx1]}${alphanum[idx2]}`;
105
+ }
106
+ function sanitizeDomain(input) {
107
+ input = input.trim();
108
+ // Add scheme if missing to make it a valid URL
109
+ if (!input.includes('://')) {
110
+ input = 'https://' + input;
111
+ }
112
+ let parsed;
113
+ try {
114
+ parsed = new URL(input);
115
+ }
116
+ catch (err) {
117
+ return {
118
+ result: '',
119
+ error: new Error(`invalid domain: ${err instanceof Error ? err.message : String(err)}`)
120
+ };
121
+ }
122
+ // Extract host and convert to lowercase
123
+ const domain = parsed.hostname.toLowerCase();
124
+ if (!domain) {
125
+ return { result: '', error: new Error('empty domain') };
126
+ }
127
+ return { result: domain, error: null };
128
+ }
@@ -0,0 +1,14 @@
1
+ export declare enum Reason {
2
+ CodeTooShort = "Code Too Short",
3
+ InvalidDomain = "Invalid Domain",
4
+ VoucherNotEligible = "Voucher Not Eligible For The User",
5
+ InvalidPhone = "Invalid Phone Number",
6
+ InvalidOrderAmount = "Invalid Order Amount"
7
+ }
8
+ export declare namespace Reason {
9
+ /**
10
+ * Returns the string representation of a Reason.
11
+ * If the input is empty/undefined/null, returns Reason.Unknown.
12
+ */
13
+ function String(r?: Reason | string | null): string | null;
14
+ }
@@ -0,0 +1,21 @@
1
+ export var Reason;
2
+ (function (Reason) {
3
+ Reason["CodeTooShort"] = "Code Too Short";
4
+ Reason["InvalidDomain"] = "Invalid Domain";
5
+ Reason["VoucherNotEligible"] = "Voucher Not Eligible For The User";
6
+ Reason["InvalidPhone"] = "Invalid Phone Number";
7
+ Reason["InvalidOrderAmount"] = "Invalid Order Amount";
8
+ })(Reason || (Reason = {}));
9
+ (function (Reason) {
10
+ /**
11
+ * Returns the string representation of a Reason.
12
+ * If the input is empty/undefined/null, returns Reason.Unknown.
13
+ */
14
+ function String(r) {
15
+ if (r === undefined || r === null || r === "") {
16
+ return null;
17
+ }
18
+ return String(r);
19
+ }
20
+ Reason.String = String;
21
+ })(Reason || (Reason = {}));
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns the SHA-256 hash of the input string.
3
+ * If the input string is already hashed, returns the string as it is.
4
+ * @param s - The string to hash
5
+ * @returns The SHA-256 hash of the input string
6
+ */
7
+ export declare function getSHA256Hash(s: string): string;
8
+ /**
9
+ * Checks if the input string is a SHA-256 hash.
10
+ * @param s - The string to check
11
+ * @returns True if the string is a SHA-256 hash, false otherwise
12
+ */
13
+ export declare function isSHA256Hash(s: string): boolean;
14
+ /**
15
+ * Generates SHA256 hash for phone number.
16
+ * If the phone number doesn't start with +91, adds +91.
17
+ * If the phone number is already hashed, returns the string as it is.
18
+ * @param phoneNum - The phone number to hash
19
+ * @returns The SHA-256 hash of the phone number
20
+ */
21
+ export declare function getPhoneNumberHash(phoneNum: string): string;
@@ -0,0 +1,44 @@
1
+ import * as crypto from 'crypto';
2
+ /**
3
+ * Returns the SHA-256 hash of the input string.
4
+ * If the input string is already hashed, returns the string as it is.
5
+ * @param s - The string to hash
6
+ * @returns The SHA-256 hash of the input string
7
+ */
8
+ export function getSHA256Hash(s) {
9
+ if (s === '' || isSHA256Hash(s)) {
10
+ return s;
11
+ }
12
+ // Create a new SHA-256 hash
13
+ const hash = crypto.createHash('sha256');
14
+ // Update the hash with the input string
15
+ hash.update(s);
16
+ // Get the hash as a hexadecimal string
17
+ return hash.digest('hex');
18
+ }
19
+ /**
20
+ * Checks if the input string is a SHA-256 hash.
21
+ * @param s - The string to check
22
+ * @returns True if the string is a SHA-256 hash, false otherwise
23
+ */
24
+ export function isSHA256Hash(s) {
25
+ // SHA-256 hash: 64-character hexadecimal string
26
+ const sha256Regex = /^[a-fA-F0-9]{64}$/;
27
+ return sha256Regex.test(s);
28
+ }
29
+ /**
30
+ * Generates SHA256 hash for phone number.
31
+ * If the phone number doesn't start with +91, adds +91.
32
+ * If the phone number is already hashed, returns the string as it is.
33
+ * @param phoneNum - The phone number to hash
34
+ * @returns The SHA-256 hash of the phone number
35
+ */
36
+ export function getPhoneNumberHash(phoneNum) {
37
+ if (phoneNum === '' || isSHA256Hash(phoneNum)) {
38
+ return phoneNum;
39
+ }
40
+ if (phoneNum.startsWith('+91')) {
41
+ return getSHA256Hash(phoneNum);
42
+ }
43
+ return getSHA256Hash('+91' + phoneNum);
44
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Converts a hex character ('0'-'9', 'a'-'f', 'A'-'F') to a number between 0-15
3
+ * @param c - The hex character to convert
4
+ * @returns A number between 0-15 representing the hex value, or 0 if the character is invalid
5
+ */
6
+ export declare function hexCharToByte(c: string): number;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Converts a hex character ('0'-'9', 'a'-'f', 'A'-'F') to a number between 0-15
3
+ * @param c - The hex character to convert
4
+ * @returns A number between 0-15 representing the hex value, or 0 if the character is invalid
5
+ */
6
+ export function hexCharToByte(c) {
7
+ // Ensure we only process the first character if a longer string is passed
8
+ const char = c.charAt(0);
9
+ if (char >= '0' && char <= '9') {
10
+ return char.charCodeAt(0) - '0'.charCodeAt(0);
11
+ }
12
+ else if (char >= 'a' && char <= 'f') {
13
+ return char.charCodeAt(0) - 'a'.charCodeAt(0) + 10;
14
+ }
15
+ else if (char >= 'A' && char <= 'F') {
16
+ return char.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
17
+ }
18
+ else {
19
+ return 0;
20
+ }
21
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@unloq/unloq-code-validtor",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/fealtyx/unloq-nodejs"
12
+ },
13
+ "homepage": "https://github.com/fealtyx/unloq-nodejs",
14
+ "bugs": {
15
+ "url": "https://github.com/fealtyx/unloq-nodejs/issues"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "jest",
20
+ "test:watch": "jest --watch"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "dependencies": {
27
+ "crypto": "^1.0.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/jest": "^30.0.0",
31
+ "@types/node": "^24.10.0",
32
+ "jest": "^30.2.0",
33
+ "ts-jest": "^29.4.5"
34
+ }
35
+ }