UNPKG

@benshi.ai/js-sdk

Version:

Benshi SDK

461 lines (404 loc) 15.1 kB
import { CartProperties, CheckoutProperties, DeliveryProperties, ECommerceTypes, ItemProperties, ItemAction, DrugProperties, ScheduleDeliveryProperties, ItemDetail, ItemType, InternalScheduleDeliveryProperties, CancelCheckoutProperties } from "./typings" import { BloodProperties } from "./blood.typings" import ECommmercePropertiesTI from './typings-ti' import BloodPropertiesTI from './blood.typings-ti' import OxygenPropertiesTI from './oxygen.typings-ti' import CommonTypesTI from '../../core/commonTypes-ti' import { injectEvent } from "../../core/injector" import BsCore from "../../core/BsCore" import { ContentBlock } from "../Navigation/typings" import { ICurrencyRepository } from "../../core/repositories/currency/CurrencyRepository" import { ICatalogRepository } from "../../core/repositories/catalog/CatalogRepository" import { createCheckers } from "ts-interface-checker" import { hasRepeatedIds, isISOLocal, toISOLocal } from "../../utils" import { OxygenProperties } from "./oxygen.typings" import { MedicalEquipmentProperties } from "./medicalEquipment.typings" const moduleName = ContentBlock.ECommerce const ERROR_CURRENCY_MISSMATCH = 'currency-missmatch' const ERROR_REPEATEAD_IDS = 'repeated-ids' let currencyRepository: ICurrencyRepository; let catalogRepository: ICatalogRepository; /** * This function must be called when the user interrupts the ordering, * at any moment * * @param properties * @param sendNow */ const logCancelCheckoutEvent = (properties: CancelCheckoutProperties, sendNow = false) => { if (hasRepeatedIds(properties.items)) { throw new Error(ERROR_REPEATEAD_IDS) } injectEvent( properties, [ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI], ECommerceTypes.Cancel, moduleName, '', sendNow ) } /** * This function must be called when the user checkout the current * cart contents * * @param {CheckoutProperties} properties * @param userId use this parameter when the logger user is not the same than the user who has created this log * @param {boolean} sendNow When true, try to send the event immediately * without enqueueing it * * @throws Will throw the error 'currency-missmatch' * when the currency of some item is different * than the currency of the whole cart * * @throws Will throw the error 'bslog-instance-not-created' * when {BsLog} have not been initialized */ const logCheckoutEvent = async (properties: CheckoutProperties, userId = '', sendNow = false): Promise<void> => { const abnormalCurrency = properties.items.find(item => item.currency !== properties.currency) if (abnormalCurrency) { throw new Error(ERROR_CURRENCY_MISSMATCH) } if (hasRepeatedIds(properties.items)) { throw new Error(ERROR_REPEATEAD_IDS) } const internalCheckoutProperties = { ...properties, usd_rate: (await currencyRepository.convertCurrencyToUSD(properties.currency)).usd } injectEvent( internalCheckoutProperties, [ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI], ECommerceTypes.Checkout, moduleName, userId, sendNow) } /** * Whenever the user adds or removes an item from the card, * this function must be called to log that event * * @param {CartProperties} properties Object with the information * to define both, the action to perform in the Cart, and the * item * @param {boolean} sendNow When true, try to send the event immediately * without enqueueing it * * @throws Will throw the error 'currency-missmatch' * when the currency of the given item is different * than the currency of the whole cart * * @throws Will throw the error 'bslog-instance-not-created' * when {BsLog} have not been initialized */ const logCartEvent = async (properties: CartProperties, sendNow = false): Promise<void> => { if (properties.currency !== properties.item.currency) { throw new Error(ERROR_CURRENCY_MISSMATCH) } const internalCartProperties = { ...properties, usd_rate: (await currencyRepository.convertCurrencyToUSD(properties.currency)).usd } injectEvent( internalCartProperties, [ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI], ECommerceTypes.Cart, moduleName, '', sendNow) } /** * Whenever the user adds or removes an item from the card, * this function must be called to log that event * * @param {ScheduleDeliveryProperties} properties Object with the information * to define this delivery * * @param userId use this parameter when the logger user is not the same than the user who has created this log * * @param {boolean} sendNow When true, try to send the event immediately * without enqueueing it * * @throws Will throw the error 'invalid-date-format' * when the delivery date does not follow RFC * 3339 long format. Ex.:1937-01-01T12:00:27.87+00:20 * */ const logScheduleDeliveryEvent = (properties: ScheduleDeliveryProperties, userId = '', sendNow = false): void => { const internalProperties: InternalScheduleDeliveryProperties = { ...properties, ts: properties.is_urgent ? toISOLocal(new Date()) : properties.ts } if (!isISOLocal(internalProperties.ts)) { throw new Error('invalid-date-format') } injectEvent( internalProperties, [ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI], ECommerceTypes.ScheduleDelivery, moduleName, userId, sendNow ) } /** * Use this method when the items have been already delivered * * * @param {DeliveryProperties} properties * @param {string} userId use this parameter when the logger user is not the same than the user who has created this log * @param {boolean} sendNow When true, try to send the event immediately * without enqueueing it * */ const logDeliveryEvent = (properties: DeliveryProperties, userId = '', sendNow = false): void => { injectEvent( properties, [ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI], ECommerceTypes.Delivery, moduleName, userId, sendNow) } /** * Use this function to log whenever the user interacts with an item * * @param { ItemProperties } properties * @param { DrugProperties } drugProperties Information related to drug features * to be injected in the server catalog. Only needed when ItemProperties.action is * `ItemAction.view` * @param { boolean } sendNow When true, try to send the event immediately * without enqueueing it * * @throws Will throw the error 'bslog-instance-not-created' * when {BsLog} have not been initialized * * @throws Will throw the error 'currency-missmatch' * when the currency of the given item is different * than the currency of the whole cart */ const logItemEvent = async ( properties: ItemProperties, productProperties: BloodProperties | DrugProperties | OxygenProperties | MedicalEquipmentProperties, sendNow = false ): Promise<void> => { const { ItemProperties: PropertiesChecker } = createCheckers( ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI, CommonTypesTI) PropertiesChecker.check(properties) const internalItemProperties = { ...properties, usd_rate: (await currencyRepository.convertCurrencyToUSD(properties.item.currency)).usd } injectEvent( internalItemProperties, [ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI], ECommerceTypes.Item, moduleName, '', sendNow) if ((properties.action === ItemAction.View || properties.action === ItemAction.Impression)) { if (properties.item.type === ItemType.Drug) { const drugProperties = productProperties as DrugProperties catalogRepository.injectDrug( properties.item.id, drugProperties) } if (properties.item.type === ItemType.Blood) { const bloodProperties = productProperties as BloodProperties catalogRepository.injectBlood( properties.item.id, bloodProperties) } if (properties.item.type === ItemType.Oxygen) { const oxygenProperties = productProperties as OxygenProperties catalogRepository.injectOxygen( properties.item.id, oxygenProperties) } if (properties.item.type === ItemType.MedicalEquipment) { const medicalEquipmentProperties = productProperties as MedicalEquipmentProperties catalogRepository.injectMedicalEquipment( properties.item.id, medicalEquipmentProperties) } } } /** * Start tracking impressions automatically for the items whose parent * is the element defined by the class `containerClassname`. A impression * is a visualization of an item for specific amount of time (i.e.: 1s) * * In addition, the items to track must include some metadata by using * the internal dataset of HTML elements, as shown in the next example. * * ```html * <div * data-log-id="0" * data-log-quantity="9" * data-log-price="426" * data-log-currency="EUR" * data-log-stock-status="in_stock" * data-log-promo-id="your-promo-id" * class="item-card" * > * ``` * * @param {string} containerClassname Represents the CSS classname of the * container to monitorize * * @param {string} itemClassname Any item within the indicated container must * also include a common classname to let the SDK monitorize it * * @throws Will throw the error 'container-does-not-exist' * when `containerClassname` does not correspond to * any DOM element */ const startTrackingImpressions = (containerClassname: string, itemClassname: string, searchId: string) => { const impressionHandler = ({ dataset, appData }) => { const { logId: id, logCurrency: currency, logType: type, logPrice: price, logQuantity: quantity, logStockStatus: stock_status, logPromoId: promo_id, logDrugCatalogObject: drug_catalog_object } = dataset const itemDetail: ItemDetail = { id, type: type || ItemType.Drug, currency: currency, price: parseInt(price), quantity: parseInt(quantity), stock_status: stock_status, promo_id: promo_id || "", } let catalogObj; let catalogData; try { catalogObj = JSON.parse(drug_catalog_object) } catch (e) { console.log('Malformed drug catalog object') return } if (type === ItemType.Drug) { catalogData = { market_id: catalogObj.market_id, name: catalogObj.name, description: catalogObj.description, supplier_id: catalogObj.supplier_id, supplier_name: catalogObj.supplier_name, producer: catalogObj.producer || "", packaging: catalogObj.packaging || "", active_ingredients: catalogObj.active_ingredients, drug_form: catalogObj.drug_form || "", drug_strength: catalogObj.drug_strength || "", atc_anatomical_group: catalogObj.atc_anatomical_group || "", otc_or_ethical: catalogObj.otc_or_ethical || "" } } else if (type === ItemType.Blood) { catalogData = { market_id: catalogObj.market_id, blood_component: catalogObj.blood_component, blood_group: catalogObj.blood_group, packaging: catalogObj.packaging, packaging_size: catalogObj.packaging_size, supplier_id: catalogObj.supplier_id || "", supplier_name: catalogObj.supplier_name || "" } } else if (type === ItemType.MedicalEquipment) { catalogData = { name: catalogObj.name, market_id: catalogObj.market_id, description: catalogObj.description || "", supplier_id: catalogObj.supplier_id, supplier_name: catalogObj.supplier_name, producer: catalogObj.producer || "", packaging: catalogObj.packaging || "", category: catalogObj.category || "" } } else if (type === ItemType.Oxygen) { catalogData = { market_id: catalogObj.market_id, packaging: catalogObj.packaging, packaging_size: catalogObj.packaging_size, supplier_id: catalogObj.supplier_id || "", supplier_name: catalogObj.supplier_name || "" } } logItemEvent({ action: ItemAction.Impression, item: itemDetail, ...appData }, catalogData) } BsCore.getInstance().startTrackingImpressions(impressionHandler, containerClassname, itemClassname, searchId) } /** * When the container indicated in `startTrackingImpressions` is no longer * available (i.e.: the user has jumped to another section), call this function * to stop monitoring those items inside the removed container * * @param {string} containerClassname The className that identifies the container * to stop tracking */ const stopTrackingImpressions = (containerClassname) => { BsCore.getInstance().stopTrackingImpressions(containerClassname) } /** * When the search is updated, that is, the user introduced a new query * or a new attribute in the available selector, but the container does not * changed, call the this function. It may happen that some items are shared * with the previous search but, due searchId has changed, they will be already * logged * * @param {string} searchId * * @throws `unknown-container` when the `containerClassname` was not being already tracked */ const restartTrackingImpressions = (containerClassname: string, searchId: string) => { BsCore.getInstance().restartTrackingImpressions(containerClassname, searchId) } /** * Private function to inject repositories * * @ignore */ const init = ( injectedCurrencyRepository: ICurrencyRepository, injectedCatalogRepository: ICatalogRepository ) => { currencyRepository = injectedCurrencyRepository catalogRepository = injectedCatalogRepository } export default { logCancelCheckoutEvent, logCartEvent, logCheckoutEvent, logDeliveryEvent, logItemEvent, logScheduleDeliveryEvent, restartTrackingImpressions, startTrackingImpressions, stopTrackingImpressions, init }