Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/app/utils/validators.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { helpers } from '@vuelidate/validators';
import { phoneNumberSchema } from '@webitel/api-services/validations';

export const macValidator = (value) => {
if (typeof value === 'undefined' || value === null || value === '') {
Expand Down Expand Up @@ -46,7 +47,7 @@ export const phoneNumberSymbolsValidator = (value) => {
if (typeof value === 'undefined' || value === null || value === '') {
return true;
}
return /^\+?[A-Za-z0-9\-_.!~*'()]+$/.test(value);
return phoneNumberSchema.safeParse(value).success;
};

export const sipPasswordSymbolsValidator = (value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ import {
OutboundResourcesAPI as ResourcesAPI,
} from '@webitel/api-services/api';
import type { EngineMemberCommunication } from '@webitel/api-services/gen/models';
import { memberCommunicationSchema } from '@webitel/api-services/validations';
import { EngineCommunicationChannels } from '@webitel/api-services/gen/models';
import {
memberCommunicationSchema,
phoneMemberCommunicationSchema,
} from '@webitel/api-services/validations';
import { WtObject } from '@webitel/ui-sdk/enums';
import { computed, ref, toRaw, watch } from 'vue';
import { useI18n } from 'vue-i18n';
Expand Down Expand Up @@ -137,7 +141,17 @@ watch(
},
);

const { r$ } = useRegleSchema(draft, memberCommunicationSchema, {
const channels = ref<Record<string, EngineCommunicationChannels>>({});

const schema = computed(() => {
const typeId = draft.value.type?.id;

return typeId && channels.value[typeId] === EngineCommunicationChannels.Phone
? phoneMemberCommunicationSchema
: memberCommunicationSchema;
});

const { r$ } = useRegleSchema(draft, schema, {
autoDirty: true,
syncState: {
onValidate: true,
Expand All @@ -164,8 +178,22 @@ const save = async () => {
close();
};

const loadCommunicationTypes = (params: unknown) =>
CommunicationsAPI.getLookup(params);
const loadCommunicationTypes = async (params: Record<string, unknown>) => {
const response = await CommunicationsAPI.getLookup({
...params,
fields: [
'id',
'name',
'channel',
],
});

for (const { id, channel } of response.items) {
if (id && channel) channels.value[id] = channel;
}

return response;
};
const loadResources = (params: unknown) => ResourcesAPI.getLookup(params);
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ const communicationTypes = [
{
id: '10',
code: 'phone',
channel: 'Phone',
},
{
id: '20',
code: 'email',
channel: 'Email',
},
];

Expand Down Expand Up @@ -158,6 +160,57 @@ describe('useNormalizeCsvMembers', () => {
).rejects.toThrow(RangeError);
});

it('rejects a dialed destination holding symbols a number cannot use', async () => {
const { normalizeData } = setup();

await expect(
normalizeData([
row({
destination: [
'380 00 1',
],
code: [
'phone',
],
}),
]),
).rejects.toThrow(SyntaxError);
});

it('accepts a dialed destination made of the allowed symbols', async () => {
const { normalizeData } = setup();

const [member] = await normalizeData([
row({
destination: [
"+38(000)-1_2.3!4~5*6'7",
],
code: [
'phone',
],
}),
]);

expect(member.communications[0].destination).toBe("+38(000)-1_2.3!4~5*6'7");
});

it('leaves a destination on a channel that is not dialed alone', async () => {
const { normalizeData } = setup();

const [member] = await normalizeData([
row({
destination: [
'joe@example.dev',
],
code: [
'email',
],
}),
]);

expect(member.communications[0].destination).toBe('joe@example.dev');
});

it('rejects dtmf that is not digits or w', async () => {
const { normalizeData } = setup();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { CommunicationsAPI, QueueMembersAPI } from '@webitel/api-services/api';
import { EngineCommunicationChannels } from '@webitel/api-services/gen/models';
import {
memberCommunicationSchema,
phoneNumberSchema,
} from '@webitel/api-services/validations';
import type { Ref } from 'vue';

/** carried over verbatim; see memberCommunicationSchema for the same pattern */
const dtmfPattern = /^[\d|w|W]*$/;
const { dtmf: dtmfSchema } = memberCommunicationSchema.shape;
const destinationSchemaByChannel = {
[EngineCommunicationChannels.Phone]: phoneNumberSchema,
} as const;

interface MappingField {
name: string;
Expand All @@ -13,11 +20,8 @@ interface MappingField {
// biome-ignore lint/suspicious/noExplicitAny: rows come from a user-supplied csv
type CsvRow = Record<string, any>;

const findCommunicationIdByCode = (
communications: CsvRow[],
code: string,
): string | undefined =>
communications.find((communication) => communication.code === code)?.id;
const findCommunicationByCode = (communications: CsvRow[], code: string) =>
communications.find((communication) => communication.code === code);

/**
* Turns parsed csv rows into queue members.
Expand All @@ -40,6 +44,12 @@ export const useNormalizeCsvMembers = ({
const normalizeData = async (data: CsvRow[]) => {
const { items: allCommunications } = await CommunicationsAPI.getList({
size: 5000,
// `channel` decides which rule a destination is held to
fields: [
'id',
'code',
'channel',
],
});

return data.map((item) => {
Expand Down Expand Up @@ -93,19 +103,33 @@ export const useNormalizeCsvMembers = ({
);

for (let index = 0; index < communicationCount; index += 1) {
const id = findCommunicationIdByCode(
const type = findCommunicationByCode(
allCommunications,
normalized.code[index],
);

if (!id) {
if (!type) {
console.error(`cannot find communication: ${normalized.code[index]}`);
}
const id = type?.id;
const destination = normalized.destination[index];

// a communication needs both a type and somewhere to reach
if (!id || !normalized.destination[index]) continue;
if (!id || !destination) continue;

const destinationSchema =
destinationSchemaByChannel[
type.channel as keyof typeof destinationSchemaByChannel
];
const checked = destinationSchema?.safeParse(destination);

if (checked && !checked.success) {
// already localized: `configureZod` installs a global error map
throw new SyntaxError(checked.error.issues[0].message);
}

const communication: CsvRow = {
destination: normalized.destination[index],
destination,
type: {
id,
},
Expand All @@ -118,7 +142,7 @@ export const useNormalizeCsvMembers = ({
communication.description = normalized.description[index];
}
if (normalized.dtmf?.[index]) {
if (!dtmfPattern.test(normalized.dtmf[index])) {
if (!dtmfSchema.safeParse(normalized.dtmf[index]).success) {
throw new SyntaxError('No valid DTMF were passed!');
}
communication.dtmf = normalized.dtmf[index];
Expand Down
Loading