@bisondesk/website-commons-sdk 1.0.36 → 1.0.38
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/lib/types/fields.d.ts +3 -1
- package/lib/types/fields.d.ts.map +1 -1
- package/lib/types/fields.js +1 -0
- package/lib/types/fields.js.map +1 -1
- package/lib/types/mailing-list.d.ts +78 -0
- package/lib/types/mailing-list.d.ts.map +1 -0
- package/lib/types/mailing-list.js +26 -0
- package/lib/types/mailing-list.js.map +1 -0
- package/lib/types/search.d.ts +1 -0
- package/lib/types/search.d.ts.map +1 -1
- package/lib/types/search.js.map +1 -1
- package/lib/types/user-searches.d.ts +49 -6
- package/lib/types/user-searches.d.ts.map +1 -1
- package/lib/types/user-searches.js +1 -1
- package/lib/types/user-searches.js.map +1 -1
- package/lib/types/user.d.ts +73 -10
- package/lib/types/user.d.ts.map +1 -1
- package/lib/types/user.js +82 -0
- package/lib/types/user.js.map +1 -1
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/vehicle.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/types/fields.ts +2 -0
- package/src/types/mailing-list.ts +53 -0
- package/src/types/search.ts +1 -0
- package/src/types/user-searches.ts +1 -1
- package/src/types/user.test.ts +128 -0
- package/src/types/user.ts +110 -16
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const emailSchema = z.string().email().min(1).toLowerCase();
|
|
4
|
+
|
|
5
|
+
export const Contact = z.object({
|
|
6
|
+
email: emailSchema,
|
|
7
|
+
status: z.enum(['unsubscribed', 'subscribed', 'pending']).optional(),
|
|
8
|
+
fields: z
|
|
9
|
+
.object({
|
|
10
|
+
Country: z.string().nullable().optional(),
|
|
11
|
+
PreferredLanguage: z.string().nullable().optional(),
|
|
12
|
+
FirstName: z.string().nullable().optional(),
|
|
13
|
+
LastName: z.string().nullable().optional(),
|
|
14
|
+
})
|
|
15
|
+
.transform((data) => {
|
|
16
|
+
const normalizedData = Object.fromEntries(
|
|
17
|
+
Object.entries(data).filter(([, value]) => value !== undefined)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return normalizedData;
|
|
21
|
+
}),
|
|
22
|
+
tags: z.record(z.string(), z.boolean()).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type Contact = z.input<typeof Contact>;
|
|
26
|
+
|
|
27
|
+
export const CheckUserIsSubscribedRequest = z.object({
|
|
28
|
+
email: emailSchema,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type CheckUserIsSubscribedRequest = z.infer<typeof CheckUserIsSubscribedRequest>;
|
|
32
|
+
|
|
33
|
+
export type ContactStatus = z.infer<typeof Contact.shape.status>;
|
|
34
|
+
|
|
35
|
+
export type CheckUserIsSubscribedResponse = {
|
|
36
|
+
createdAt?: string;
|
|
37
|
+
status?: 'unsubscribed' | 'subscribed' | 'pending';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type UpsertContactResponse = {
|
|
41
|
+
subscribed: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SubscribeContactRequest = z.object({
|
|
45
|
+
email: emailSchema,
|
|
46
|
+
language: z.string(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type SubscribeContactRequest = z.infer<typeof SubscribeContactRequest>;
|
|
50
|
+
|
|
51
|
+
export type SubscribeContactResponse = {
|
|
52
|
+
subscribedAt: string;
|
|
53
|
+
};
|
package/src/types/search.ts
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { UserDetails, VatSchema, isValidVatForCountry } from './user.js';
|
|
2
|
+
|
|
3
|
+
const companyVatSchema = UserDetails.shape.company;
|
|
4
|
+
|
|
5
|
+
describe('VatSchema (standalone)', () => {
|
|
6
|
+
describe('valid', () => {
|
|
7
|
+
test.each([
|
|
8
|
+
{ input: 'PT123', expected: 'PT123' },
|
|
9
|
+
{ input: 'gb123', expected: 'GB123' },
|
|
10
|
+
{ input: 'X', expected: 'X' },
|
|
11
|
+
{ input: 'X', expected: 'X' },
|
|
12
|
+
])('$input -> $expected', ({ input, expected }) => {
|
|
13
|
+
const result = VatSchema.safeParse(input);
|
|
14
|
+
expect(result.success).toBe(true);
|
|
15
|
+
expect(result.data).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('empty string coerces to undefined', () => {
|
|
20
|
+
const result = VatSchema.safeParse('');
|
|
21
|
+
expect(result.success).toBe(true);
|
|
22
|
+
expect(result.data).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('isValidVatForCountry', () => {
|
|
27
|
+
test.each([
|
|
28
|
+
{ vat: 'PT123', country: 'PT', expected: true },
|
|
29
|
+
{ vat: 'DE123', country: 'DE', expected: true },
|
|
30
|
+
{ vat: 'EL123', country: 'GR', expected: true },
|
|
31
|
+
{ vat: 'IE1234567X', country: 'IE', expected: true },
|
|
32
|
+
{ vat: 'GB123', country: 'GB', expected: true },
|
|
33
|
+
{ vat: 'XI123456789', country: 'GB', expected: true },
|
|
34
|
+
{ vat: 'CHE123456789', country: 'CH', expected: true },
|
|
35
|
+
{ vat: 'NO123456789', country: 'NO', expected: true },
|
|
36
|
+
{ vat: 'ANYTHING', country: 'US', expected: true },
|
|
37
|
+
{ vat: '1234567890', country: 'UA', expected: true },
|
|
38
|
+
{ vat: '123456789', country: 'BY', expected: true },
|
|
39
|
+
{ vat: '123456789', country: 'RS', expected: true },
|
|
40
|
+
{ vat: 'DE123', country: 'PT', expected: false },
|
|
41
|
+
{ vat: 'GR123', country: 'GR', expected: false },
|
|
42
|
+
{ vat: 'PT', country: 'PT', expected: false },
|
|
43
|
+
{ vat: 'CH123', country: 'CH', expected: false },
|
|
44
|
+
{ vat: 'XI', country: 'GB', expected: false },
|
|
45
|
+
])('vat=$vat country=$country -> $expected', ({ vat, country, expected }) => {
|
|
46
|
+
expect(isValidVatForCountry({ vat, country })).toBe(expected);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('UserDetails (full schema)', () => {
|
|
51
|
+
const validBase = {
|
|
52
|
+
email: 'test@example.com',
|
|
53
|
+
company: { country: 'PT', vat: 'PT123456789' },
|
|
54
|
+
marketingChannels: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
test('parses valid full payload', () => {
|
|
58
|
+
const result = UserDetails.safeParse(validBase);
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.data?.email).toBe('test@example.com');
|
|
61
|
+
expect(result.data?.company.vat).toBe('PT123456789');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('lowercases email', () => {
|
|
65
|
+
const result = UserDetails.safeParse({ ...validBase, email: 'TEST@EXAMPLE.COM' });
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
expect(result.data?.email).toBe('test@example.com');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('company vat invalid does not affect other fields parsing', () => {
|
|
71
|
+
const result = UserDetails.safeParse({
|
|
72
|
+
...validBase,
|
|
73
|
+
company: { country: 'PT', vat: 'DE123' },
|
|
74
|
+
});
|
|
75
|
+
expect(result.success).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('company without vat still valid', () => {
|
|
79
|
+
const result = UserDetails.safeParse({ ...validBase, company: { country: 'PT' } });
|
|
80
|
+
expect(result.success).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('UserDetails company vat (with country)', () => {
|
|
85
|
+
describe('valid', () => {
|
|
86
|
+
test.each([
|
|
87
|
+
{ input: { country: 'PT', vat: 'PT123456789' } },
|
|
88
|
+
{ input: { country: 'DE', vat: 'DE123' } },
|
|
89
|
+
{ input: { country: 'gr', vat: 'el123' }, expectedVat: 'EL123' },
|
|
90
|
+
{ input: { country: 'IE', vat: 'IE1234567X' } },
|
|
91
|
+
{ input: { country: 'GB', vat: 'GB123456789' } },
|
|
92
|
+
{ input: { country: 'GB', vat: 'XI123456789' } },
|
|
93
|
+
{ input: { country: 'CH', vat: 'CHE123456789' } },
|
|
94
|
+
{ input: { country: 'NO', vat: 'NO123456789' } },
|
|
95
|
+
{ input: { country: 'US', vat: 'ANYTHING123' } },
|
|
96
|
+
{ input: { country: 'UA', vat: '1234567890' } },
|
|
97
|
+
{ input: { country: 'BY', vat: '123456789' } },
|
|
98
|
+
{ input: { country: 'RS', vat: '123456789' } },
|
|
99
|
+
{ input: { country: 'BR', vat: 'abc' }, expectedVat: 'ABC' },
|
|
100
|
+
{ input: { vat: 'SOMETHING' } },
|
|
101
|
+
{ input: { vat: '' } },
|
|
102
|
+
{ input: {} },
|
|
103
|
+
])('$input', ({ input, expectedVat }) => {
|
|
104
|
+
const result = companyVatSchema.safeParse(input);
|
|
105
|
+
expect(result.success).toBe(true);
|
|
106
|
+
if (expectedVat) {
|
|
107
|
+
expect(result.data?.vat).toBe(expectedVat);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('invalid', () => {
|
|
113
|
+
test.each([
|
|
114
|
+
{ input: { country: 'PT', vat: 'DE123' } },
|
|
115
|
+
{ input: { country: 'DE', vat: 'DE' } },
|
|
116
|
+
{ input: { country: 'GR', vat: 'GR123' } },
|
|
117
|
+
{ input: { country: 'CH', vat: 'CH123' } },
|
|
118
|
+
{ input: { country: 'GB', vat: 'XI' } },
|
|
119
|
+
])('$input', ({ input }) => {
|
|
120
|
+
const result = companyVatSchema.safeParse(input);
|
|
121
|
+
expect(result.success).toBe(false);
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
const issue = result.error.issues[0];
|
|
124
|
+
expect((issue as any).params).toEqual({ messageKey: 'vat.invalidFormat' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/types/user.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { MarketingChannel } from './index.js';
|
|
2
3
|
|
|
3
4
|
export type AuthRequest = {
|
|
@@ -10,20 +11,113 @@ export enum FleetSize {
|
|
|
10
11
|
Large = '6+',
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
export const VatSchema = z.string().toUpperCase();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Light VAT-number validation patterns by country.
|
|
18
|
+
*
|
|
19
|
+
* Countries without an entry accept any VAT number (no validation).
|
|
20
|
+
*
|
|
21
|
+
* Includes EU-27, EEA non-EU (NO, IS), Switzerland (CH), and the UK (GB).
|
|
22
|
+
* Note: Liechtenstein shares the Swiss CHE VAT system and is intentionally
|
|
23
|
+
* excluded — its numbers don't start with "LI".
|
|
24
|
+
*/
|
|
25
|
+
const VAT_PATTERNS: Record<string, RegExp> = {
|
|
26
|
+
// EU-27
|
|
27
|
+
AT: /^AT.+/,
|
|
28
|
+
BE: /^BE.+/,
|
|
29
|
+
BG: /^BG.+/,
|
|
30
|
+
CY: /^CY.+/,
|
|
31
|
+
CZ: /^CZ.+/,
|
|
32
|
+
DE: /^DE.+/,
|
|
33
|
+
DK: /^DK.+/,
|
|
34
|
+
EE: /^EE.+/,
|
|
35
|
+
ES: /^ES.+/,
|
|
36
|
+
FI: /^FI.+/,
|
|
37
|
+
FR: /^FR.+/,
|
|
38
|
+
GR: /^EL.+/,
|
|
39
|
+
HR: /^HR.+/,
|
|
40
|
+
HU: /^HU.+/,
|
|
41
|
+
IE: /^IE.+/,
|
|
42
|
+
IT: /^IT.+/,
|
|
43
|
+
LT: /^LT.+/,
|
|
44
|
+
LU: /^LU.+/,
|
|
45
|
+
LV: /^LV.+/,
|
|
46
|
+
MT: /^MT.+/,
|
|
47
|
+
NL: /^NL.+/,
|
|
48
|
+
PL: /^PL.+/,
|
|
49
|
+
PT: /^PT.+/,
|
|
50
|
+
RO: /^RO.+/,
|
|
51
|
+
SE: /^SE.+/,
|
|
52
|
+
SI: /^SI.+/,
|
|
53
|
+
SK: /^SK.+/,
|
|
54
|
+
// EEA non-EU
|
|
55
|
+
NO: /^NO.+/,
|
|
56
|
+
IS: /^IS.+/,
|
|
57
|
+
// Switzerland — 3-letter CHE prefix
|
|
58
|
+
CH: /^CHE.+/,
|
|
59
|
+
// United Kingdom + Northern Ireland (XI prefix post-Brexit)
|
|
60
|
+
GB: /^(GB|XI).+/,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const isValidVatForCountry = ({
|
|
64
|
+
vat,
|
|
65
|
+
country,
|
|
66
|
+
}: {
|
|
67
|
+
vat: string;
|
|
68
|
+
country: string;
|
|
69
|
+
}): boolean => {
|
|
70
|
+
const pattern = VAT_PATTERNS[country.toUpperCase()];
|
|
71
|
+
if (!pattern) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return pattern.test(vat);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getVatPrefix = (country: string): string => {
|
|
78
|
+
const pattern = VAT_PATTERNS[country.toUpperCase()];
|
|
79
|
+
if (!pattern) {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
const match = pattern.toString().match(/\^(\w+)/);
|
|
83
|
+
return match ? match[1] : '';
|
|
29
84
|
};
|
|
85
|
+
|
|
86
|
+
export const UserDetails = z.object({
|
|
87
|
+
firstName: z.string().optional(),
|
|
88
|
+
lastName: z.string().optional(),
|
|
89
|
+
phone: z.string().optional(),
|
|
90
|
+
preferredLanguage: z.string().optional(),
|
|
91
|
+
fleetSize: z.nativeEnum(FleetSize).optional(),
|
|
92
|
+
trader: z.boolean().optional(),
|
|
93
|
+
email: z.string().email().toLowerCase(),
|
|
94
|
+
company: z
|
|
95
|
+
.object({
|
|
96
|
+
country: z.string().optional(),
|
|
97
|
+
city: z.string().optional(),
|
|
98
|
+
name: z.string().optional(),
|
|
99
|
+
vat: VatSchema.optional(),
|
|
100
|
+
postalCode: z.string().optional(),
|
|
101
|
+
})
|
|
102
|
+
/* TODO when updating to zod v4, change addIssue options to
|
|
103
|
+
message: 'vat.invalidFormat',
|
|
104
|
+
path: ['vat'],
|
|
105
|
+
*/
|
|
106
|
+
.superRefine((data, ctx) => {
|
|
107
|
+
if (!data.vat || !data.country) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isValidVatForCountry({ vat: data.vat, country: data.country })) {
|
|
111
|
+
const prefix = getVatPrefix(data.country);
|
|
112
|
+
|
|
113
|
+
ctx.addIssue({
|
|
114
|
+
code: z.ZodIssueCode.custom,
|
|
115
|
+
path: ['vat'],
|
|
116
|
+
params: { prefix, messageKey: 'vat.invalidFormat' },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
marketingChannels: z.array(z.nativeEnum(MarketingChannel)),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export type UserDetails = z.infer<typeof UserDetails>;
|