@benshi.ai/js-sdk
Version:
Benshi SDK
461 lines (404 loc) • 15.1 kB
text/typescript
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
}