Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { WebSocketMessageValue } from '$features/websockets/models';
import type { BillingPlan, ChangePlanRequest, ChangePlanResult } from '$lib/generated/api';
import type { BillingPlan, ChangePlanRequest, ChangePlanResult, StringValueFromBody } from '$lib/generated/api';
import type { QueryClient } from '@tanstack/svelte-query';

import { accessToken } from '$features/auth/index.svelte';
Expand All @@ -24,6 +24,7 @@ export async function invalidateOrganizationQueries(queryClient: QueryClient, me
export const queryKeys = {
adminSearch: (params: GetAdminSearchOrganizationsParams) => [...queryKeys.list(params.mode), 'admin', { ...params }] as const,
changePlan: (id: string | undefined) => [...queryKeys.type, id, 'change-plan'] as const,
data: (id: string | undefined) => [...queryKeys.type, id, 'data'] as const,
deleteOrganization: (ids: string[] | undefined) => [...queryKeys.ids(ids), 'delete'] as const,
id: (id: string | undefined, mode: 'stats' | undefined) => (mode ? ([...queryKeys.type, id, { mode }] as const) : ([...queryKeys.type, id] as const)),
ids: (ids: string[] | undefined) => [...queryKeys.type, ...(ids ?? [])] as const,
Expand Down Expand Up @@ -51,6 +52,16 @@ export interface ChangePlanMutationRequest {
};
}

export interface DeleteOrganizationDataParams {
key: string;
}

export interface DeleteOrganizationDataRequest {
route: {
id: string | undefined;
};
}

export interface DeleteOrganizationRequest {
route: {
ids: string[];
Expand Down Expand Up @@ -133,6 +144,17 @@ export interface PatchOrganizationRequest {
};
}

export interface PostOrganizationDataParams {
key: string;
value: string;
}

export interface PostOrganizationDataRequest {
route: {
id: string | undefined;
};
}

export interface PostSetBonusOrganizationParams {
bonusEvents: number;
expires?: Date;
Expand Down Expand Up @@ -216,6 +238,35 @@ export function deleteOrganization(request: DeleteOrganizationRequest) {
}));
}

export function deleteOrganizationData(request: DeleteOrganizationDataRequest) {
const queryClient = useQueryClient();

return createMutation<boolean, ProblemDetails, DeleteOrganizationDataParams>(() => ({
enabled: () => !!accessToken.current && !!request.route.id,
mutationFn: async ({ key }: DeleteOrganizationDataParams) => {
const client = useFetchClient();
const response = await client.delete(`organizations/${request.route.id}/data/${encodeURIComponent(key)}`);
return response.ok;
},
mutationKey: queryKeys.data(request.route.id),
onError: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.id(request.route.id, undefined) });
},
onSuccess: (_, { key }) => {
updateOrganizationQueryData(queryClient, request.route.id, (organization) => {
if (!organization.data) {
return organization;
}

const data = { ...organization.data };
delete data[key];

return { ...organization, data };
});
}
}));
}

export function deleteOrganizationUser(request: DeleteOrganizationUserRequest) {
const queryClient = useQueryClient();
return createMutation<void, ProblemDetails, void>(() => ({
Expand Down Expand Up @@ -424,6 +475,32 @@ export function postOrganization() {
}));
}

export function postOrganizationData(request: PostOrganizationDataRequest) {
const queryClient = useQueryClient();

return createMutation<boolean, ProblemDetails, PostOrganizationDataParams>(() => ({
enabled: () => !!accessToken.current && !!request.route.id,
mutationFn: async ({ key, value }: PostOrganizationDataParams) => {
const client = useFetchClient();
const response = await client.post(`organizations/${request.route.id}/data/${encodeURIComponent(key)}`, <StringValueFromBody>{ value });
return response.ok;
},
mutationKey: queryKeys.data(request.route.id),
onError: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.id(request.route.id, undefined) });
},
onSuccess: (_, { key, value }) => {
updateOrganizationQueryData(queryClient, request.route.id, (organization) => ({
...organization,
data: {
...(organization.data ?? {}),
[key]: value
}
}));
}
}));
}

export function postSetBonusOrganization() {
const queryClient = useQueryClient();

Expand Down Expand Up @@ -521,3 +598,14 @@ export function setOrganizationFeature(request: SetOrganizationFeatureRequest) {
}
}));
}

function updateOrganizationQueryData(
queryClient: ReturnType<typeof useQueryClient>,
id: string | undefined,
updater: (organization: ViewOrganization) => ViewOrganization
) {
// Keep both cached variants in sync because the same organization can be loaded with or without stats mode.
for (const mode of [undefined, 'stats'] as const) {
queryClient.setQueryData<undefined | ViewOrganization>(queryKeys.id(id, mode), (organization) => (organization ? updater(organization) : organization));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';

import { getOrganizationBillingInformation, normalizeOrganizationBillingInformationValue, organizationBillingInformationDataKeys } from './billing-information';

describe('getOrganizationBillingInformation', () => {
it('returns billing information from known organization data keys', () => {
// Arrange
const organization = {
data: {
[organizationBillingInformationDataKeys.address]: '123 Main Street',
[organizationBillingInformationDataKeys.name]: 'Acme, Inc.',
[organizationBillingInformationDataKeys.vatId]: 'DE123456789',
[organizationBillingInformationDataKeys.vatNumber]: '123456789'
}
};

// Act
const billingInformation = getOrganizationBillingInformation(organization);

// Assert
expect(billingInformation).toEqual({
address: '123 Main Street',
name: 'Acme, Inc.',
vatId: 'DE123456789',
vatNumber: '123456789'
});
});

it('defaults missing or non-string billing information values to empty strings', () => {
// Arrange
const organization = {
data: {
[organizationBillingInformationDataKeys.address]: ['invalid'],
[organizationBillingInformationDataKeys.name]: null,
[organizationBillingInformationDataKeys.vatId]: undefined,
[organizationBillingInformationDataKeys.vatNumber]: 42
}
};

// Act
const billingInformation = getOrganizationBillingInformation(organization);

// Assert
expect(billingInformation).toEqual({
address: '',
name: '',
vatId: '',
vatNumber: ''
});
});
});

describe('normalizeOrganizationBillingInformationValue', () => {
it('trims non-empty values and removes blank values', () => {
// Arrange
const value = ' DE123456789 ';
const blankValue = ' ';

// Act
const normalizedValue = normalizeOrganizationBillingInformationValue(value);
const normalizedBlankValue = normalizeOrganizationBillingInformationValue(blankValue);

// Assert
expect(normalizedValue).toBe('DE123456789');
expect(normalizedBlankValue).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ViewOrganization } from './models';

export const organizationBillingInformationDataKeys = {
address: 'billing_address',
name: 'billing_name',
vatId: 'billing_vat_id',
vatNumber: 'billing_vat_number'
} as const;

export interface OrganizationBillingInformation {
address: string;
name: string;
vatId: string;
vatNumber: string;
}

export function getOrganizationBillingInformation(organization?: null | Pick<ViewOrganization, 'data'>): OrganizationBillingInformation {
const data = organization?.data;

return {
address: getOrganizationBillingInformationValue(data?.[organizationBillingInformationDataKeys.address]),
name: getOrganizationBillingInformationValue(data?.[organizationBillingInformationDataKeys.name]),
vatId: getOrganizationBillingInformationValue(data?.[organizationBillingInformationDataKeys.vatId]),
vatNumber: getOrganizationBillingInformationValue(data?.[organizationBillingInformationDataKeys.vatNumber])
};
}

export function normalizeOrganizationBillingInformationValue(value: string): null | string {
const trimmedValue = value.trim();
return trimmedValue || null;
}

function getOrganizationBillingInformationValue(value: unknown): string {
return typeof value === 'string' ? value : '';
}
Loading