All files / libs/reporting/custom-reports/shared/src/lib/business customReports.utils.ts

67.62% Statements 94/139
90% Branches 18/20
55.55% Functions 5/9
67.62% Lines 94/139

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 1401x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x     8x 8x 8x 1x 1x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x     5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 1x 1x 1x 1x                                                                               1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 12x 12x 1x 12x 1x 12x 10x 12x 12x  
import { isNil, last, round } from 'lodash-es';
 
import { FormatsEnum } from '@amalia/data-capture/fields/types';
import { dayjs } from '@amalia/ext/dayjs';
import { isCurrencyValue, type CurrencyValue } from '@amalia/kernel/monetary/types';
 
import {
  CustomReportSourceIdentifier,
  type CustomReport,
  type CustomReportColumn,
  type CustomReportConfigurationField,
  type CustomReportDataSourceManifest,
  type CustomReportManifestsMap,
  type CustomReportRow,
  type CustomReportValue,
} from '../types/customReport';
 
export const getCustomReportFieldDefinition = (
  customReport: Pick<CustomReport, 'configuration' | 'source'>,
  manifestsMap: CustomReportManifestsMap,
  field: { identifier: string; joins?: string[] },
): CustomReportDataSourceManifest['fields'][0] | null => {
  const source = (field.joins?.length ? last(field.joins) : customReport.source) as string;
  const manifest = manifestsMap[source];
 
  // Manifest may not be loaded yet.
  if (!manifest) {
    return null;
  }
 
  return manifest.fields.find((fd) => fd.identifier === field.identifier) ?? null;
};
 
export const makeUniqueCustomReportFieldIdentifier = (
  source: CustomReportSourceIdentifier,
  fieldConfiguration: Pick<CustomReportConfigurationField, 'identifier' | 'joins'>,
) => {
  const tableAlias = fieldConfiguration.joins?.length ? last(fieldConfiguration.joins) : source;
 
  return `${tableAlias}__${fieldConfiguration.identifier}`;
};
 
export const getCustomReportColumnsDefinitions = (
  customReport: Pick<CustomReport, 'configuration' | 'source'>,
  manifestsMap: CustomReportManifestsMap,
) =>
  customReport.configuration.fields
    .map((fieldConfiguration) => {
      const fieldDefinition = getCustomReportFieldDefinition(customReport, manifestsMap, fieldConfiguration);
 
      // Some fields may have disappeared from the manifest. Remove them to avoid crashes.
      if (!fieldDefinition) {
        return null;
      }
 
      return {
        fieldDefinition,
        field: fieldConfiguration,
        identifier: makeUniqueCustomReportFieldIdentifier(customReport.source, fieldConfiguration),
      };
    })
    .filter(Boolean);
 
export function buildCustomReportColumns(
  customReport: Pick<CustomReport, 'configuration' | 'source'>,
  manifestsMap: CustomReportManifestsMap,
): CustomReportColumn[] {
  return getCustomReportColumnsDefinitions(customReport, manifestsMap).map(
    ({ fieldDefinition, field, identifier }) => ({
      identifier,
      label: field.alias ?? fieldDefinition.label,
      format: fieldDefinition.format,
    }),
  );
}
 
export function transformReportDataToTable(
  columns: CustomReportColumn[],
  items: CustomReportRow[],
  precision: number = 2,
  addCurrencySymbol: boolean = true,
) {
  return [
    columns.map((column) => column.label),
    ...items.map((row) =>
      columns.map((field) => {
        // for each field, take the value from the row
        const rowValue = row[field.identifier];

        switch (field.format) {
          case FormatsEnum.currency:
            return isNil(rowValue) || !isCurrencyValue(rowValue) || isNil(rowValue.value)
              ? ''
              : addCurrencySymbol
                ? round(rowValue.value, precision) + rowValue.symbol
                : round(rowValue.value, precision);

          case FormatsEnum.number:
          case FormatsEnum.percent:
            return isNil(rowValue) ? 0 : round(+rowValue, precision);

          case FormatsEnum.date:
            // If the value is a timestamp, format it as a date.
            // Otherwise just return the value as is.
            return !isNil(rowValue) &&
              dayjs(`${rowValue as Exclude<CustomReportValue, CurrencyValue>}`, 'X', true).isValid()
              ? dayjs.utc(`${rowValue as Exclude<CustomReportValue, CurrencyValue>}`, 'X').format('YYYY-MM-DD')
              : `${(rowValue as Exclude<CustomReportValue, CurrencyValue>) ?? ''}`;

          default:
            return `${(rowValue as Exclude<CustomReportValue, CurrencyValue>) ?? ''}`;
        }
      }),
    ),
  ];
}
 
// PSQL max identifier length is 63 characters.
// We need to truncate the identifier to 63 characters to avoid errors.
export const formatMaxLengthIdentifierPsql = (identifier: string): string => identifier.substring(0, 63);
 
export const stringifyCustomReportFieldPosition = ({ identifier, joins }: CustomReportConfigurationField): string =>
  JSON.stringify({ identifier, joins });
 
export const parseCustomReportFieldPosition = (value: string): CustomReportConfigurationField => ({
  joins: undefined,
  ...(JSON.parse(value) as CustomReportConfigurationField),
});
 
export const getCustomReportJoinForUser = (source: CustomReportSourceIdentifier) => {
  switch (source) {
    case CustomReportSourceIdentifier.RULE_METRIC:
      return 'ruleMetricUser';
    case CustomReportSourceIdentifier.STATEMENT:
      return 'statementUser';
    default:
      throw new Error('This custom report source is not supported');
  }
};