{ "version": 3, "sources": ["libs/inventory-ui/src/lib/active-items/active-items.service.ts", "libs/inventory-ui/src/lib/common/item-label/item-label.component.ts", "libs/inventory-ui/src/lib/common/item-label/item-label.component.html", "libs/inventory-ui/src/lib/common/item-list/item-list.component.ts", "libs/inventory-ui/src/lib/common/item-list/item-list.component.html", "libs/inventory-ui/src/lib/active-items/active-items.component.ts", "libs/inventory-ui/src/lib/active-items/active-items.component.html", "libs/inventory-ui/src/lib/common/edition-selector/edition-selector.component.ts", "libs/inventory-ui/src/lib/common/edition-selector/edition-selector.component.html", "libs/inventory-ui/src/lib/common/edition-selector/find-item-edition.pipe.ts", "libs/inventory-ui/src/lib/common/frequency-selector/frequency-selector.component.ts", "libs/inventory-ui/src/lib/common/frequency-selector/frequency-selector.component.html", "libs/inventory-ui/src/lib/common/pipes/billing-period.pipe.ts", "libs/inventory-ui/src/lib/common/pipes/cents-to-dollars.pipe.ts", "libs/inventory-ui/src/lib/common/item-price/frequency.ts", "libs/inventory-ui/src/lib/item-pricing-table/item-pricing-table.utils.ts", "libs/inventory-ui/src/lib/common/pipes/price-validator.pipe.ts", "libs/inventory-ui/src/lib/common/price-summary-display/price-summary-display.component.ts", "libs/inventory-ui/src/lib/common/price-summary-display/price-summary-display.component.html", "libs/inventory-ui/src/lib/common/item-common.module.ts", "libs/inventory-ui/src/lib/active-items/active-items.module.ts", "libs/inventory-ui/src/lib/common/inventory-item/adapters/store.ts", "libs/inventory-ui/src/lib/common/inventory-item/adapters/marketplace.ts", "libs/inventory-ui/src/lib/common/inventory-item/adapters/adapter.ts", "libs/inventory-ui/src/lib/item-selector/util/item.ts", "libs/inventory-ui/src/lib/item-selector/util/edition.ts", "libs/inventory-ui/src/lib/services/package-contents.service.ts", "libs/inventory-ui/src/lib/package-contents-popover/package-contents-popover.component.ts", "libs/inventory-ui/src/lib/package-contents-popover/package-contents-popover.component.html", "libs/inventory-ui/src/lib/package-contents-popover/package-contents-popover.module.ts", "libs/inventory-ui/src/lib/billing-term-dialog/sales-to-galaxy-frequency.pipe.ts", "libs/inventory-ui/src/lib/common/item-price/retail.ts", "libs/inventory-ui/src/lib/common/item-price/wholesale.ts", "libs/inventory-ui/src/lib/item-pricing-table/item-pricing-table.interface.ts", "libs/inventory-ui/src/lib/item-pricing-table/item-pricing-table.data-source.ts", "libs/inventory-ui/src/lib/item-pricing-table/multi-activatable-dialog/multi-activatable-dialog.component.ts", "libs/inventory-ui/src/lib/item-pricing-table/multi-activatable-dialog/multi-activatable-dialog.component.html", "libs/inventory-ui/src/lib/item-pricing-table/pipes/format-wholesale-minimum-hint.pipe.ts", "libs/inventory-ui/src/lib/item-pricing-table/pipes/has-setup-fee.pipe.ts", "libs/inventory-ui/src/lib/item-pricing-table/pipes/is-setup-fee-removable.pipe.ts", "libs/inventory-ui/src/lib/item-pricing-table/pipes/format-billing-term.pipe.ts", "libs/inventory-ui/src/lib/item-pricing-table/item-pricing-table.component.ts", "libs/inventory-ui/src/lib/item-pricing-table/item-pricing-table.component.html", "libs/inventory-ui/src/lib/item-pricing-table/item-pricing-table.module.ts", "libs/inventory-ui/src/lib/i18n/assets/en_devel.json", "libs/inventory-ui/src/lib/i18n/inventory-ui-i18n.module.ts", "libs/inventory-ui/src/lib/services/item-conflict.service.ts", "libs/inventory-ui/src/lib/services/item-dependency.service.ts", "libs/inventory-ui/src/lib/services/line-item-cache.service.ts", "libs/inventory-ui/src/lib/item-selector/item-selector-v2.service.ts", "libs/inventory-ui/src/lib/item-selector/interface.ts", "libs/inventory-ui/src/lib/item-selector/item-selector-columns.ts", "libs/inventory-ui/src/lib/item-selector/item-selector.data-source.ts", "libs/inventory-ui/src/lib/item-selector/pipes/format-product-type.pipe.ts", "libs/inventory-ui/src/lib/item-selector/pipes/format-product-requirement.pipe.ts", "libs/inventory-ui/src/lib/item-selector/pipes/parse-wholesale-currency.pipe.ts", "libs/inventory-ui/src/lib/item-selector/pipes/is-item-disabled.pipe.ts", "libs/inventory-ui/src/lib/item-selector/pipes/format-disabled-item.pipe.ts", "libs/inventory-ui/src/lib/item-selector/item-selector.component.ts", "libs/inventory-ui/src/lib/item-selector/item-selector.component.html", "libs/inventory-ui/src/lib/item-selector/item-selector.module.ts", "libs/inventory-ui/src/lib/item-selection-cart/item-selection-cart.component.ts", "libs/inventory-ui/src/lib/item-selection-cart/item-selection-cart.component.html", "libs/inventory-ui/src/lib/item-selection-cart/item-selection-cart.module.ts", "libs/inventory-ui/src/lib/services/services.module.ts", "libs/inventory-ui/src/lib/billing-term-dialog/billing-term-dialog.component.ts", "libs/inventory-ui/src/lib/billing-term-dialog/billing-term-dialog.component.html", "libs/inventory-ui/src/lib/billing-term-dialog/format-revenue-period.pipe.ts", "libs/inventory-ui/src/lib/item-selector/item-selector-dialog/item-selector-dialog.component.ts", "libs/inventory-ui/src/lib/item-selector/item-selector-dialog/item-selector-dialog.component.html", "libs/inventory-ui/src/lib/item-pricing-table-selector/item-pricing-table-selector.service.ts", "libs/inventory-ui/src/lib/item-pricing-table/line-item.ts", "libs/inventory-ui/src/lib/item-pricing-table-selector/item-pricing-table-selector.component.ts", "libs/inventory-ui/src/lib/item-pricing-table-selector/item-pricing-table-selector.component.html", "libs/inventory-ui/src/lib/item-selector/item-selector-dialog/item-selector-dialog.module.ts", "libs/inventory-ui/src/lib/item-pricing-table-selector/item-pricing-table-selector.module.ts", "libs/inventory-ui/src/lib/item-search-select-filter/item-search-multi-select-filter/item-search-multi-select-filter.component.ts", "libs/inventory-ui/src/lib/item-search-select-filter/item-search-multi-select-filter/item-search-multi-select-filter.component.html", "libs/inventory-ui/src/lib/item-search-select-filter/item-search-select-filter.module.ts", "libs/inventory-ui/src/lib/item-whitelabel/item-whitelabel.ts", "libs/inventory-ui/src/lib/item-whitelabel/item-whitelabel.service.ts", "libs/store/src/lib/assets/i18n/en_devel.json", "libs/store/src/lib/shared/constants.ts", "libs/store/src/lib/pricing/base-pricing.component.ts", "libs/store/src/lib/pricing/highlight-pricing/highlight-pricing.component.ts", "libs/store/src/lib/pricing/highlight-pricing/highlight-pricing.component.html", "libs/store/src/lib/shared/format-date.pipe.ts", "libs/store/src/lib/shared/format-discount.pipe.ts", "libs/store/src/lib/pricing/table-pricing/table-pricing.component.ts", "libs/store/src/lib/pricing/table-pricing/table-pricing.component.html", "libs/store/src/lib/pricing/pricing.component.ts", "libs/store/src/lib/pricing/pricing.component.html", "libs/store/src/lib/shared/conversion-utils.ts", "libs/store/src/lib/pricing/pricing.module.ts", "libs/store/src/lib/store-card.component.ts", "libs/store/src/lib/store-card.module.ts", "libs/store/src/lib/header-container/header-container.component.ts", "libs/store/src/lib/header-container/header-container.component.html", "libs/store/src/lib/header-container/header-container.module.ts", "libs/store/src/lib/order-form/dropdown-form-section/temporary-storage.service.ts", "libs/store/src/lib/order-form/fields/field.component.ts", "libs/store/src/lib/order-form/fields/field.component.html", "libs/store/src/lib/order-form/dropdown-form-section/dropdown-form-section.component.ts", "libs/store/src/lib/order-form/fields/field-base.ts", "libs/store/src/lib/order-form/fields/field-builder/field-builder.component.ts", "libs/store/src/lib/order-form/fields/field-builder/field-builder.component.html", "libs/store/src/lib/order-form/fields/field.service.ts", "libs/store/src/lib/order-form/dropdown-form-section/dropdown-form-section-data.ts", "libs/store/src/lib/order-form/order-form-section/order-form-section-data.ts", "libs/store/src/lib/order-form/order-form/interface.ts", "libs/store/src/lib/shared/build-temporary-storage-key.pipe.ts", "libs/store/src/lib/order-form/order-form-section/order-form-section.component.ts", "libs/store/src/lib/order-form/order-form-section/order-form-section.component.html", "libs/store/src/lib/order-form/fields/field-checkbox.ts", "libs/store/src/lib/order-form/fields/field-dropdown.ts", "libs/store/src/lib/order-form/fields/field-fileuploadgroup.ts", "libs/store/src/lib/order-form/fields/field-textarea.ts", "libs/store/src/lib/order-form/fields/field-textbox.ts", "libs/store/src/lib/order-form/fields/field-vbcuser.ts", "libs/store/src/lib/order-form/order-form/order-form.service.ts", "libs/store/src/lib/order-form/order-form/order-form.component.ts", "libs/store/src/lib/order-form/order-form/order-form.component.html", "libs/store/src/lib/order-form/order-item-list/order-item-compact/order-item-compact.component.ts", "libs/store/src/lib/order-form/order-item-list/order-item-compact/order-item-compact.component.html", "libs/store/src/lib/order-form/order-item-list/discount-helpers.ts", "libs/store/src/lib/order-form/order-item-list/order-item-list.component.ts", "libs/store/src/lib/order-form/order-item-list/order-item-list.component.html", "libs/store/src/lib/order-form/order-form.module.ts", "libs/store/src/lib/stencils/details-stencil.component.ts", "libs/store/src/lib/stencils/card-stencil.component.ts", "libs/store/src/lib/stencils/card-list-stencil.component.ts", "libs/store/src/lib/stencils/search-bar-stencil.component.ts", "libs/store/src/lib/stencils/list-stencil.component.ts", "libs/store/src/lib/stencils/stencils.module.ts", "libs/store/src/lib/addon-list/addon-list.module.ts", "libs/store/src/lib/store.service.ts", "libs/store/src/lib/lmi-categories.ts", "libs/store/src/lib/store.module.ts", "libs/store/src/lib/files/shared/file.model.ts", "libs/store/src/lib/shared/sellable.ts", "libs/store/src/lib/shared/case-transform.ts", "libs/store/src/lib/shared/product.ts", "libs/store/src/lib/package-dropdown/package-dropdown.module.ts", "libs/store/src/lib/restrictions/country-v2/supported-countries-v2.component.ts", "libs/store/src/lib/restrictions/country-v2/supported-countries-v2.component.html", "libs/store/src/lib/restrictions/restrictions.module.ts", "libs/store/src/lib/product-details-v2/product-details-v2.interface.ts", "libs/store/src/lib/store-item.ts", "libs/store/src/lib/product-details-v2/screenshots-files/screenshots-files.component.ts", "libs/store/src/lib/product-details-v2/screenshots-files/screenshots-files.component.html", "libs/store/src/lib/product-details-v2/form-preview/form-preview.component.ts", "libs/store/src/lib/product-details-v2/form-preview/form-preview.component.html", "libs/store/src/lib/selling-info/selling-info.component.ts", "libs/store/src/lib/faqs/faqs.component.ts", "libs/store/src/lib/product-details-v2/need-help/need-help.component.ts", "libs/store/src/lib/product-details-v2/videos/video-parser.ts", "libs/store/src/lib/product-details-v2/videos/videos.component.ts", "libs/store/src/lib/product-details-v2/videos/videos.component.html", "libs/store/src/lib/product-details-v2/addon-list/addon-list.component.ts", "libs/store/src/lib/product-details-v2/addon-list/addon-list.component.html", "libs/store/src/lib/product-details-v2/product-info/required-products-list/required-products-list.component.ts", "libs/store/src/lib/product-details-v2/product-info/required-products-list/required-products-list.component.html", "libs/store/src/lib/product-details-v2/product-info/product-requirements/app-product-requirements.component.ts", "libs/store/src/lib/product-details-v2/product-info/product-requirements/app-product-requirements.component.html", "libs/store/src/lib/product-details-v2/product-info/product-info.component.ts", "libs/store/src/lib/product-details-v2/product-info/product-info.component.html", "libs/store/src/lib/product-details-v2/editions-marketing/editions-marketing.component.ts", "libs/store/src/lib/product-details-v2/editions-marketing/editions-marketing.component.html", "libs/store/src/lib/product-details-v2/product-details-v2.component.ts", "libs/store/src/lib/product-details-v2/product-details-v2.component.html", "libs/store/src/lib/faqs/faqs.module.ts", "libs/store/src/lib/files/files.component.ts", "libs/store/src/lib/files/files.module.ts", "libs/store/src/lib/selling-info/selling-info.module.ts", "libs/store/src/lib/product-details-v2/product-details-v2.module.ts", "libs/store/src/lib/products-nav/products-nav.component.ts", "libs/store/src/lib/products-nav/products-nav.component.html", "libs/store/src/lib/products-nav/products-nav-v2/products-nav-v2.component.ts", "libs/store/src/lib/products-nav/products-nav-v2/products-nav-v2.component.html", "libs/store/src/lib/products-nav/products-nav.module.ts", "libs/store/src/lib/package-details-v2/package.ts", "libs/store/src/lib/package-details-v2/package-details-v2.component.ts", "libs/store/src/lib/package-details-v2/package-details-v2.component.html", "libs/store/src/lib/package-details-v2/package-details-v2.module.ts", "libs/store/src/lib/package-details/package.ts", "libs/store/src/lib/shared/reseller-item.ts", "libs/store/src/lib/order-summary/order-summary.service.ts", "libs/store/src/lib/order-summary/contract-estimated-total/contract-estimated-total.component.ts", "libs/store/src/lib/order-summary/contract-estimated-total/contract-estimated-total.component.html", "libs/store/src/lib/order-summary/order-summarized-charges/order-summarized-charges.component.ts", "libs/store/src/lib/order-summary/order-summarized-charges/order-summarized-charges.component.html", "libs/store/src/lib/order-summary/order-summarized-totals/order-summarized-totals.component.ts", "libs/store/src/lib/order-summary/order-summarized-totals/order-summarized-totals.component.html", "libs/store/src/lib/order-summary/order-summary.component.ts", "libs/store/src/lib/order-summary/order-summary.component.html", "libs/store/src/lib/order-summary/order-summary.module.ts", "libs/store/src/lib/manage-product/manage-product-table/table.util.ts", "libs/store/src/lib/manage-product/manage-product-table/manage-product-table.component.ts", "libs/store/src/lib/manage-product/manage-product-table/manage-product-table.component.html", "libs/store/src/lib/manage-product/manage-product.module.ts", "libs/store/src/lib/utils/billing-utils.ts", "libs/store/src/lib/manage-product/manage-product-table/product-information.ts"], "sourcesContent": ["import { inject, Injectable } from '@angular/core';\nimport { combineLatest, forkJoin, Observable, of } from 'rxjs';\nimport { map, switchMap, withLatestFrom } from 'rxjs/operators';\nimport {\n AccountDates,\n AccountsService,\n AddonActivation,\n AppAndAddonActivationStatus,\n ListAppsAndAddonsActivationsStatusesForBusinessResponseAppsAndAddonsActivationStatusesInterface as ActivationStatusesInterface,\n} from '@vendasta/accounts/legacy';\nimport { Account as AccountProductDetails } from '@vendasta/accounts/legacy/_internal/objects/api';\nimport { App, AppKey, GetMultiAppRequest, PartnerApiService } from '@vendasta/marketplace-apps';\nimport { Activation } from './type';\nimport { InventoryListItem } from '../common/item-list/type';\n\n@Injectable()\nexport class ActiveItemsService {\n private accountsService = inject(AccountsService);\n private partnerMarketplaceService = inject(PartnerApiService);\n\n getCurrentlyActiveItems(partnerId: string, businessId: string, marketId: string): Observable {\n const appStatuses$ = this.listAppStatuses(businessId);\n\n const appKeys$ = appStatuses$.pipe(\n map((appStatuses) =>\n appStatuses.map(\n (appStatus) =>\n new AppKey({\n appId: appStatus.appId,\n editionId: appStatus.editionId,\n }),\n ),\n ),\n );\n\n const items$ = appKeys$.pipe(\n switchMap((appKeys) =>\n this.partnerMarketplaceService.getMultiApp(\n new GetMultiAppRequest({\n partnerId: partnerId,\n marketId: marketId,\n appKeys: appKeys,\n includeNotEnabled: true,\n projectionFilter: {\n paths: ['editionInformation', 'sharedMarketingInformation', 'trialConfiguration'],\n },\n }),\n ),\n ),\n );\n\n const activations$ = this.listActivationsForBusiness(businessId, partnerId);\n\n return combineLatest([items$, activations$]).pipe(\n withLatestFrom(appStatuses$),\n map(([[resp, activations], appStatuses]) => {\n const apps = resp?.apps ?? [];\n const appStatusMap = this.mapify(\n appStatuses,\n (as: ActivationStatusesInterface) => as.appId,\n );\n const activationsMap = this.mapify(activations, (ac: Activation) => ac.addonId || ac.appId);\n return apps.map((app: App) =>\n this.convertToInventoryListItem(app, appStatusMap.get(app.key.appId), activationsMap.get(app.key.appId)),\n );\n }),\n );\n }\n\n listAppStatuses(businessId: string): Observable {\n return this.accountsService.listAppsAndAddonsActivationStatusesForBusiness(businessId).pipe(\n map((appStatuses) => {\n if (!appStatuses) return [];\n return appStatuses.filter((appStatus) => this.checkIfActive(appStatus));\n }),\n );\n }\n\n listActivationsForBusiness(businessId: string, partnerId: string): Observable {\n const accountDates$ = this.accountsService.listAccountDates(businessId, '', 1000);\n\n const appActivations$ = this.listActiveProducts(businessId, partnerId).pipe(\n map((acc) => this.convertAppActivationsToActivations(acc)),\n );\n\n const addonActivations$ = appActivations$.pipe(\n map((appActivations) =>\n appActivations.map((act) =>\n this.accountsService.listAddonActivations(businessId, act.appId).pipe(map((resp) => resp.activations || [])),\n ),\n ),\n switchMap((responses: Observable[]) => {\n if (responses.length === 0) return of([]);\n\n return forkJoin([...responses]).pipe(\n map((arrays) => {\n const addonActivations: AddonActivation[] = [];\n return addonActivations.concat(addonActivations, ...arrays);\n }),\n );\n }),\n map((addonActivations) => this.convertAddonActivationsToActivations(addonActivations)),\n );\n\n return combineLatest([appActivations$, addonActivations$, accountDates$]).pipe(\n map(([appActivations, addonActivations, accountDates]) => {\n return appActivations\n .concat(addonActivations)\n .map((activation) => this.mergeActivationsAndDates(activation, accountDates));\n }),\n );\n }\n\n listActiveProducts(businessId: string, partnerId: string): Observable {\n return this.accountsService.list(businessId, partnerId).pipe(map((listResponse) => listResponse.accounts));\n }\n\n private convertToInventoryListItem(\n app: App,\n appStatus: ActivationStatusesInterface | undefined,\n activation: Activation | undefined,\n ): InventoryListItem {\n const activeItem: InventoryListItem = { itemId: '', type: 'app', name: '', iconUrl: '', quantity: 0 };\n\n activeItem.itemId = app.key.appId;\n activeItem.name = app.sharedMarketingInformation.name;\n activeItem.iconUrl = app.sharedMarketingInformation.iconUrl;\n\n if (app.sharedMarketingInformation.editionName) {\n activeItem.edition = { editionId: app.key.editionId, name: app.sharedMarketingInformation.editionName };\n }\n\n if (appStatus) {\n activeItem.quantity = appStatus.count ?? 0;\n\n if (appStatus.status === AppAndAddonActivationStatus.PENDING) {\n activeItem.status = 'Pending';\n }\n if (appStatus.status === AppAndAddonActivationStatus.CANCELED) {\n activeItem.status = 'Canceled';\n }\n }\n\n if (activation) {\n const isTrial = activation.trial;\n if (activation.deactivation) {\n const currentTime = new Date();\n const trialStatusIsActive = activation.deactivation > currentTime;\n if (isTrial) {\n activeItem.status = trialStatusIsActive ? 'Demo' : 'Demo expired';\n }\n }\n activeItem.descriptor = activation.activationDescriptor;\n }\n\n return activeItem;\n }\n\n private convertAppActivationsToActivations(accounts: AccountProductDetails[]): Activation[] {\n if (!accounts) return [];\n return accounts.map((acc) => ({ ...acc, appId: this.getAppId(acc) }));\n }\n\n private convertAddonActivationsToActivations(activations: AddonActivation[]): Activation[] {\n if (!activations) return [];\n\n const now = new Date();\n return activations\n .filter((activation) => !activation.deactivated || activation.deactivated > now)\n .map((a) => ({ ...a, activation: a.activated, deactivation: a.deactivated }));\n }\n\n private getAppId(account: AccountProductDetails): string {\n if (account.productId === 'MP') {\n return account.appId;\n } else if (account.productId === 'CP') {\n return account.accountId;\n } else {\n return account.productId;\n }\n }\n\n private checkIfActive(appStatus: ActivationStatusesInterface): boolean {\n const isActivated = appStatus.status === AppAndAddonActivationStatus.ACTIVATED;\n const isCancelled = appStatus.status === AppAndAddonActivationStatus.CANCELED;\n const isPending = appStatus.status === AppAndAddonActivationStatus.PENDING;\n return isActivated || isCancelled || isPending;\n }\n\n private mergeActivationsAndDates(activation: Activation, accountDates: AccountDates[]): Activation {\n if (!accountDates || accountDates.length < 1) return activation;\n\n const date =\n accountDates.find((item) => item.activationId === activation.activationId) ||\n accountDates.find((item) => item.appId === activation.appId && item.addonId === activation.addonId);\n if (date) {\n if (!activation.deactivation || activation.deactivation.getFullYear() <= 1900) {\n activation.deactivation = date.pendingDeactivation;\n }\n }\n return activation;\n }\n\n private mapify(array: T[], keyFn: (item: T) => string | undefined): Map {\n return array.reduce((map, item) => {\n const key = keyFn(item);\n if (key !== undefined) {\n map.set(key, item);\n }\n return map;\n }, new Map());\n }\n}\n", "import { Component, Input, OnInit } from '@angular/core';\nimport { LabeledItem } from './item-label.interface';\n\n@Component({\n selector: 'inventory-ui-item-label',\n templateUrl: './item-label.component.html',\n styleUrls: ['./item-label.component.scss'],\n})\nexport class ItemLabelComponent implements OnInit {\n @Input() item?: LabeledItem;\n @Input() showItemId = true;\n @Input() showEditionName = true;\n @Input() showStatus = false;\n\n itemStatus = '';\n\n ngOnInit(): void {\n if (this.showStatus) {\n let statusKey = '';\n switch (this.item?.status) {\n case 'Pending':\n statusKey = 'ACTIVE_ITEMS.STATUS.PENDING';\n break;\n case 'Canceled':\n statusKey = 'ACTIVE_ITEMS.STATUS.CANCELED';\n break;\n case 'Demo':\n statusKey = 'ACTIVE_ITEMS.STATUS.DEMO';\n break;\n case 'Demo expired':\n statusKey = 'ACTIVE_ITEMS.STATUS.DEMO_EXPIRED';\n break;\n }\n this.itemStatus = statusKey;\n }\n }\n}\n", "
\n \n
\n
\n \n {{ item.name }}\n | {{ item.editionName }}\n \n 1\" class=\"item-quantity\">({{ item.quantity }})\n
\n {{ item.subtext }}\n {{ item.itemId }}\n \n {{ itemStatus | translate }}\n \n \n
\n
\n", "import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { InventoryListItem } from './type';\n\nexport interface ItemListItemRemovedEvent {\n itemId: string;\n parentItemId?: string;\n isNested: boolean;\n}\n\n@Component({\n selector: 'inventory-ui-item-list',\n templateUrl: './item-list.component.html',\n styleUrls: ['./item-list.component.scss'],\n})\nexport class ItemListComponent {\n @Input() items: InventoryListItem[] = [];\n @Input() removeButtonEnabled = false;\n @Input() showStatus = false;\n\n @Output() itemRemoved = new EventEmitter();\n\n removeItem(itemId: string, parentItemId: string, isNested: boolean): void {\n this.itemRemoved.emit({ itemId, parentItemId, isNested });\n }\n}\n", "\n \n \n \n
\n \n
\n
\n \n
\n\n\n 0\" data-cy=\"item-row\">\n
\n \n \n \n clear\n \n \n
\n
\n
\n", "import { Component, inject, Input, OnInit } from '@angular/core';\nimport { ActiveItemsService } from './active-items.service';\nimport { Observable } from 'rxjs';\nimport { InventoryListItem } from '../common/item-list/type';\n\n@Component({\n selector: 'inventory-ui-active-items',\n templateUrl: './active-items.component.html',\n styleUrls: ['./active-items.component.scss'],\n})\nexport class ActiveItemsComponent implements OnInit {\n private activeItemsService = inject(ActiveItemsService);\n\n @Input() partnerId = '';\n @Input() businessId = '';\n @Input() marketId = '';\n currentlyActiveItems$!: Observable;\n\n ngOnInit(): void {\n this.currentlyActiveItems$ = this.activeItemsService.getCurrentlyActiveItems(\n this.partnerId,\n this.businessId,\n this.marketId,\n );\n }\n}\n", "
\n

\n {{ 'ACTIVE_ITEMS.TITLE' | translate }}\n

\n
\n\n\n \n\n", "import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { MatSelectChange } from '@angular/material/select';\nimport { InventoryItemEdition } from '../inventory-item/item-types/app';\n\nexport interface Edition extends InventoryItemEdition {\n isActive?: boolean;\n}\n\n@Component({\n selector: 'inventory-ui-edition-selector',\n templateUrl: './edition-selector.component.html',\n styleUrls: ['./edition-selector.component.scss'],\n})\nexport class EditionSelectorComponent {\n @Input() selectedEditionId!: string;\n @Input() editions!: Edition[];\n\n @Output() editionSelectionChange = new EventEmitter();\n\n handleEditionChange(event: MatSelectChange): void {\n this.editionSelectionChange.emit(event.value);\n }\n}\n", "\n \n \n \n \n {{ edition?.name }}\n \n \n {{ 'ACTIVE_ITEMS.STATUS.ACTIVE' | translate }}\n \n \n \n \n \n \n {{ edition?.name }}\n \n \n {{ 'ACTIVE_ITEMS.STATUS.ACTIVE' | translate }}\n \n \n \n \n\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { Edition } from './edition-selector.component';\n\n@Pipe({\n name: 'findItemEdition',\n pure: false,\n})\nexport class FindItemEditionPipe implements PipeTransform {\n transform(selectedEditionId: string, editions: Edition[]): Edition | undefined {\n return editions?.find((edition) => (edition?.editionId ?? '') === (selectedEditionId ?? ''));\n }\n}\n", "import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { Frequency as GalaxyFrequency } from '@vendasta/galaxy/frequency';\n\n@Component({\n selector: 'inventory-ui-frequency-selector',\n templateUrl: './frequency-selector.component.html',\n styleUrls: ['./frequency-selector.component.scss'],\n})\nexport class FrequencySelectorComponent {\n @Input() selectedFrequency: GalaxyFrequency = GalaxyFrequency.MONTHLY;\n\n @Output() frequencySelectionChange = new EventEmitter();\n\n frequencyOptions = [\n { label: 'BILLING_FREQUENCY.ONE_TIME', value: GalaxyFrequency.ONE_TIME },\n { label: 'BILLING_FREQUENCY.MONTHLY', value: GalaxyFrequency.MONTHLY },\n { label: 'BILLING_FREQUENCY.YEARLY', value: GalaxyFrequency.YEARLY },\n ];\n\n handleBillingFrequencyChange(frequency: GalaxyFrequency): void {\n this.frequencySelectionChange.emit(frequency);\n }\n}\n", "\n \n \n {{ frequency.label | translate }}\n \n \n\n", "import { inject, Pipe, PipeTransform } from '@angular/core';\nimport { BillingPeriodInterface, RevenuePeriod } from '@vendasta/sales-orders';\nimport { TranslateService } from '@ngx-translate/core';\nimport { DatePipe } from '@angular/common';\nimport dayjs from 'dayjs';\n\n@Pipe({\n name: 'formatBillingPeriod',\n})\nexport class FormatBillingPeriodPipe implements PipeTransform {\n private datePipe = inject(DatePipe);\n private translateService = inject(TranslateService);\n\n transform(billingPeriod?: BillingPeriodInterface, revenuePeriod?: RevenuePeriod): string {\n if (!billingPeriod) {\n return '';\n }\n\n const startDate = this.datePipe.transform(billingPeriod.startDate, 'MMM d, yyyy');\n if (!startDate) {\n return '';\n }\n\n if (\n !revenuePeriod ||\n !billingPeriod.endDate ||\n dayjs(billingPeriod.endDate).isSame(0) ||\n dayjs(billingPeriod.endDate).isBefore(startDate)\n ) {\n return startDate;\n }\n\n if (revenuePeriod === RevenuePeriod.YEARLY) {\n return this.formatYearlyBillingPeriod(startDate, billingPeriod.endDate);\n }\n\n return this.formatMonthlyBillingPeriod(startDate, billingPeriod.endDate);\n }\n\n private formatMonthlyBillingPeriod(startDate: string, endDate: Date): string {\n const numMonths = dayjs(endDate).diff(startDate, 'month');\n const translationProps: BillingPeriodTranslationProps = { startDate, numPeriods: numMonths };\n if (numMonths <= 0) {\n return startDate;\n }\n if (numMonths === 1) {\n return this.translateService.instant('BILLING_PERIOD.START_DATE_ONE_MONTH', translationProps);\n }\n\n return this.translateService.instant('BILLING_PERIOD.START_DATE_MANY_MONTHS', translationProps);\n }\n\n private formatYearlyBillingPeriod(startDate: string, endDate: Date): string {\n const numYears = dayjs(endDate).diff(startDate, 'year');\n const translationProps: BillingPeriodTranslationProps = { startDate, numPeriods: numYears };\n if (numYears <= 0) {\n return startDate;\n }\n if (numYears === 1) {\n return this.translateService.instant('BILLING_PERIOD.START_DATE_ONE_YEAR', translationProps);\n }\n\n return this.translateService.instant('BILLING_PERIOD.START_DATE_MANY_YEARS', translationProps);\n }\n}\n\nexport type BillingPeriodTranslationProps = {\n startDate: string;\n numPeriods: number;\n};\n", "import { Pipe, PipeTransform } from '@angular/core';\n\n@Pipe({\n name: 'centsToDollars',\n})\nexport class CentsToDollarsPipe implements PipeTransform {\n transform(value: number): number {\n return value < 0 ? 0 : value / 100;\n }\n}\n", "import { AppPrices, FrequencyPrice, InstancePrice } from '@galaxy/marketplace-apps';\nimport { SimplePrice, getPartnerCurrency } from '@vendasta/billing-ui';\n\nexport type ItemFrequencyPrice = SimplePrice & {\n total: number;\n minimumPrice?: number;\n};\n\nexport function getFrequencyPrices(appPrices: AppPrices, editionId = '', pricingContext: string): FrequencyPrice[] {\n const appPrice = appPrices?.pricesForContexts?.[pricingContext];\n const priceForEditions = appPrice?.pricesForEditions?.[editionId];\n const currency = getPartnerCurrency(appPrice);\n return priceForEditions?.pricesForCurrencies?.[currency]?.pricesForFrequencies ?? [];\n}\n\n// instance price is automatically calculated based on: quantity, discounts, and setup fee\n// to get the discounted unit price, we need to reverse engineer the instance price\nexport function calculateUnitPriceFromInstancePrice(instancePrice: InstancePrice, setupFee = 0, quantity = 1): number {\n const totalPricePerUnit = (instancePrice?.total ?? 0) / quantity;\n const basePricePerUnit = totalPricePerUnit - setupFee;\n return basePricePerUnit;\n}\n", "import { Frequency } from '@vendasta/galaxy/frequency';\nimport { ItemFeeRow, ItemRow, PriceSummary } from './item-pricing-table.interface';\nimport { ItemFrequencyPrice } from '../common/item-price';\nimport { AppPrices, FeeAmountType, FrequencyPrice } from '@vendasta/marketplace-apps';\nimport { getFrequencyPrices } from '../common/item-price';\nimport { toGalaxyFrequency } from '@vendasta/billing-ui';\nimport { InventoryItemWithPricing } from '../common/inventory-item';\n\nexport function parseItemFees(appPrices: AppPrices, item: InventoryItemWithPricing): ItemFeeRow[] {\n const editionId = item.type === 'app' ? item.edition?.editionId : undefined;\n const retailFrequencyPrices = getFrequencyPrices(appPrices, editionId, 'retail');\n const wholesaleFrequencyPrices = getFrequencyPrices(appPrices, editionId, 'wholesale');\n\n const fees: ItemFeeRow[] = [];\n const setupFee = parseSetupFee(wholesaleFrequencyPrices, retailFrequencyPrices, item);\n if (setupFee) {\n fees.push(setupFee);\n }\n\n const managementFees = parseManagementFees(wholesaleFrequencyPrices, item);\n fees.push(...managementFees);\n\n return fees;\n}\n\nexport function convertHTMLControlValueToNumber(value: string | number): number {\n if (typeof value === 'string') {\n return parseFloat(value.replace(/,/g, ''));\n }\n return value;\n}\n\nexport function calculateTotalWholesalePrice(items: ItemRow[]): PriceSummary {\n const priceSummary: PriceSummary = { oneTime: 0, monthly: 0, yearly: 0, currency: '' };\n\n items.forEach((item) => {\n if (!priceSummary.currency && item.wholesalePricing.currency) {\n priceSummary.currency = item.wholesalePricing.currency;\n }\n\n item.wholesalePricing.frequencyPrices?.forEach((price) => {\n addPriceBasedOnFrequency(price, priceSummary);\n addFeesBasedOnFrequency(item, 'wholesale', priceSummary);\n });\n });\n\n return priceSummary;\n}\n\nexport function calculateTotalRetailPrice(items: ItemRow[]): PriceSummary {\n const priceSummary: PriceSummary = { oneTime: 0, monthly: 0, yearly: 0, currency: '' };\n\n items.forEach((item) => {\n if (!priceSummary.currency && item.retailPricing.currency) {\n priceSummary.currency = item.retailPricing.currency;\n }\n\n addPriceBasedOnFrequency(item.retailPricing, priceSummary);\n addFeesBasedOnFrequency(item, 'retail', priceSummary);\n });\n\n return priceSummary;\n}\n\nfunction addPriceBasedOnFrequency(itemPrice: ItemFrequencyPrice, priceSummary: PriceSummary) {\n if (!itemPrice.price) return;\n\n // one time total needs to be updated regardless of the type of frequency\n priceSummary.oneTime += itemPrice.total;\n if (itemPrice.frequency === Frequency.MONTHLY) {\n priceSummary.monthly += itemPrice.total;\n }\n if (itemPrice.frequency === Frequency.YEARLY) {\n priceSummary.yearly += itemPrice.total;\n }\n}\n\nfunction addFeesBasedOnFrequency(item: ItemRow, priceType: 'wholesale' | 'retail', priceSummary: PriceSummary) {\n item.fees.forEach((fee) => {\n if (priceType === 'wholesale') {\n const wholesaleTotal = fee.wholesaleAmount * item.quantity;\n priceSummary.oneTime += wholesaleTotal;\n if (fee.type === 'management' && fee.frequency === Frequency.MONTHLY) {\n priceSummary.monthly += wholesaleTotal;\n }\n if (fee.type === 'management' && fee.frequency === Frequency.YEARLY) {\n priceSummary.yearly += wholesaleTotal;\n }\n }\n\n if (priceType === 'retail') {\n priceSummary.oneTime += fee.retailAmountTotal ?? 0;\n }\n });\n}\n\nexport function updatePercentageManagementFee(item: ItemRow, oldWholeSalePrice: number): void {\n item.fees.forEach((fee) => {\n if (fee.type === 'management' && fee.amountType === 'percentage') {\n const managementFeePercentage = Math.round((fee.wholesaleAmount / oldWholeSalePrice) * 100);\n const frequencyPrice = item.wholesalePricing.frequencyPrices.find((p) => p.frequency === fee.frequency);\n const wholesalePrice = frequencyPrice?.price || 0;\n fee.wholesaleAmount = calculatePercentageFee(managementFeePercentage, wholesalePrice);\n }\n });\n}\n\nexport function calculatePercentageFee(percentage: number, price: number) {\n return Math.round((percentage * price) / 100);\n}\n\nfunction parseSetupFee(\n wholesalePrices: FrequencyPrice[],\n retailPrices: FrequencyPrice[],\n item: InventoryItemWithPricing,\n): ItemFeeRow | null {\n const wholesaleSetupFeeTotal = wholesalePrices.reduce((acc, price) => acc + (price.setupFee || 0), 0);\n const retailSetupFeeTotal = parseRetailSetupFee(retailPrices, item);\n\n if (!wholesaleSetupFeeTotal && !retailSetupFeeTotal) return null;\n\n return {\n type: 'setup',\n label: 'Setup fee',\n amountType: 'fixed',\n wholesaleAmount: wholesaleSetupFeeTotal,\n retailAmount: retailSetupFeeTotal,\n retailAmountTotal: retailSetupFeeTotal * item.quantity,\n frequency: Frequency.ONE_TIME,\n };\n}\n\nfunction parseRetailSetupFee(retailPrices: FrequencyPrice[], item: InventoryItemWithPricing): number {\n const revenueComponents = item.pricing?.currentRevenue?.revenueComponents;\n\n // default to marketplaceplace-apps fees if no revenue components are present\n if (!revenueComponents) return retailPrices.reduce((acc, price) => acc + (price.setupFee || 0), 0);\n\n let retailSetupFeeTotal = 0;\n revenueComponents.forEach((rc, i) => {\n // one time is the default enum value\n const isOneTime = !rc.period;\n\n // skip the first revenue component since it represents the base price\n if (i > 0 && isOneTime) {\n retailSetupFeeTotal += rc.value ?? 0;\n }\n });\n return retailSetupFeeTotal;\n}\n\nfunction parseManagementFees(wholesaleFrequencyPrices: FrequencyPrice[], item: InventoryItemWithPricing): ItemFeeRow[] {\n const managementFees: ItemFeeRow[] = [];\n\n wholesaleFrequencyPrices.forEach((price) => {\n // exclude management fees when discounted wholesale price is zero\n if (price.instancePrice && getDiscountedBasePrice(price) <= 0) {\n return;\n }\n\n price.fees?.forEach((fee) => {\n if (fee.amountType === FeeAmountType.FEE_AMOUNT_TYPE_FIXED) {\n managementFees.push({\n type: 'management',\n label: 'Management fee',\n amountType: 'fixed',\n wholesaleAmount: fee.amount,\n frequency: toGalaxyFrequency(price.frequency),\n });\n }\n\n if (fee.amountType === FeeAmountType.FEE_AMOUNT_TYPE_PERCENTAGE) {\n let wholesalePrice = price.pricingRules?.[0]?.price;\n // use custom price if it is present\n if (item.pricing?.cost?.customPrice) {\n wholesalePrice = item.pricing.cost.customPrice;\n }\n managementFees.push({\n type: 'management',\n label: `Management fee (${fee.amount}%)`,\n amountType: 'percentage',\n wholesaleAmount: (fee.amount * wholesalePrice) / 100,\n frequency: toGalaxyFrequency(price.frequency),\n });\n }\n });\n });\n\n return managementFees;\n}\n\nfunction getDiscountedBasePrice(price: FrequencyPrice): number {\n const instancePrice = price.instancePrice?.total ?? 0;\n const setupFee = price.setupFee ?? 0;\n\n return instancePrice - setupFee;\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { Validators } from '@angular/forms';\nimport { GalaxyInputValidator } from '@vendasta/galaxy/input';\nimport { convertHTMLControlValueToNumber } from '../../item-pricing-table/item-pricing-table.utils';\n\n@Pipe({ name: 'priceValidator' })\nexport class PriceValidatorPipe implements PipeTransform {\n transform(minimumPrice: number): GalaxyInputValidator[] {\n const required: GalaxyInputValidator = {\n validatorFn: Validators.required,\n errorMessage: 'Required',\n };\n const minValue: GalaxyInputValidator = {\n validatorFn: (control) => {\n const convertedPrice = convertHTMLControlValueToNumber(control.value);\n if (convertedPrice < minimumPrice) {\n return { min: true };\n }\n return null;\n },\n errorMessage: 'Cannot be lower than minimum',\n };\n return [required, minValue];\n }\n}\n", "import { Component, Input, Signal, ViewEncapsulation, computed, signal } from '@angular/core';\nimport { Frequency } from '@vendasta/galaxy/frequency';\nimport { PriceSummary } from '../../item-pricing-table/item-pricing-table.interface';\n\n@Component({\n selector: 'inventory-ui-price-summary-display',\n templateUrl: './price-summary-display.component.html',\n styleUrls: ['./price-summary-display.component.scss'],\n encapsulation: ViewEncapsulation.None,\n})\nexport class PriceSummaryDisplayComponent {\n protected readonly Frequency = Frequency;\n @Input() priceTotal: Signal = signal({\n oneTime: 0,\n monthly: 0,\n yearly: 0,\n currency: '',\n });\n\n public oneTimeCost = computed(() => {\n const priceTotal = this.priceTotal();\n return priceTotal.oneTime;\n });\n\n public monthlyCost = computed(() => {\n const priceTotal = this.priceTotal();\n return priceTotal.monthly;\n });\n\n public yearlyCost = computed(() => {\n const priceTotal = this.priceTotal();\n return priceTotal.yearly;\n });\n\n public currency = computed(() => {\n const priceTotal = this.priceTotal();\n return priceTotal.currency;\n });\n}\n", "
\n \n
\n
\n \n
\n
\n \n
\n", "import { CommonModule, DatePipe } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatChipsModule } from '@angular/material/chips';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatListModule } from '@angular/material/list';\nimport { MatSelectModule } from '@angular/material/select';\nimport { BillingUiModule } from '@vendasta/billing-ui';\nimport { GalaxyAvatarModule } from '@vendasta/galaxy/avatar';\nimport { GalaxyBadgeModule } from '@vendasta/galaxy/badge';\nimport { GalaxyFormFieldModule } from '@vendasta/galaxy/form-field';\nimport { EditionSelectorComponent } from './edition-selector/edition-selector.component';\nimport { FindItemEditionPipe } from './edition-selector/find-item-edition.pipe';\nimport { FrequencySelectorComponent } from './frequency-selector/frequency-selector.component';\nimport { ItemLabelComponent } from './item-label/item-label.component';\nimport { ItemListComponent } from './item-list/item-list.component';\nimport { FormatBillingPeriodPipe } from './pipes/billing-period.pipe';\nimport { CentsToDollarsPipe } from './pipes/cents-to-dollars.pipe';\nimport { PriceValidatorPipe } from './pipes/price-validator.pipe';\nimport { ToStringPipe } from './pipes/to-string.pipe';\nimport { PriceSummaryDisplayComponent } from './price-summary-display/price-summary-display.component';\n\nexport const ITEM_COMMON_MODULE_IMPORTS = [\n CommonModule,\n MatButtonModule,\n MatChipsModule,\n MatIconModule,\n MatListModule,\n BillingUiModule,\n GalaxyAvatarModule,\n GalaxyFormFieldModule,\n MatSelectModule,\n GalaxyBadgeModule,\n];\n\nexport const ITEM_COMMON_MODULE_PROVIDERS = [DatePipe, FormatBillingPeriodPipe];\n\nexport const ITEM_COMMON_MODULE_DECLARATIONS = [\n ItemLabelComponent,\n ItemListComponent,\n CentsToDollarsPipe,\n PriceValidatorPipe,\n ToStringPipe,\n PriceSummaryDisplayComponent,\n FormatBillingPeriodPipe,\n EditionSelectorComponent,\n FindItemEditionPipe,\n FrequencySelectorComponent,\n];\n\n@NgModule({\n imports: ITEM_COMMON_MODULE_IMPORTS,\n exports: [\n ItemLabelComponent,\n ItemListComponent,\n CentsToDollarsPipe,\n PriceValidatorPipe,\n ToStringPipe,\n PriceSummaryDisplayComponent,\n FormatBillingPeriodPipe,\n EditionSelectorComponent,\n FrequencySelectorComponent,\n ],\n declarations: ITEM_COMMON_MODULE_DECLARATIONS,\n providers: ITEM_COMMON_MODULE_PROVIDERS,\n})\nexport class ItemCommonModule {}\n", "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { ActiveItemsComponent } from './active-items.component';\nimport { ActiveItemsService } from './active-items.service';\nimport { ItemCommonModule } from '../common/item-common.module';\n\n@NgModule({\n imports: [CommonModule, ItemCommonModule, TranslateModule],\n exports: [ActiveItemsComponent],\n declarations: [ActiveItemsComponent],\n providers: [ActiveItemsService],\n})\nexport class ActiveItemsModule {}\n", "import { InventoryItemAddon, InventoryItemApp } from '../item-types';\n\n// Partial properties of a ResellerItem from @vendasta/store (circular dependency)\nexport class GenericResellerItem {\n appId?: string;\n addonId?: string;\n name?: string;\n icon?: string;\n usesEditions?: boolean;\n editionId?: string;\n\n constructor(resellerItem?: Partial) {\n Object.assign(this, resellerItem);\n }\n}\n\nexport function fromResellerItem(resellerItem: GenericResellerItem): InventoryItemApp | InventoryItemAddon {\n if (resellerItem.addonId) {\n return {\n itemId: resellerItem.addonId ?? '',\n type: 'addon',\n name: resellerItem.name ?? '',\n iconUrl: resellerItem.icon ?? '',\n parentItemId: resellerItem.appId ?? '',\n quantity: 1,\n };\n }\n\n let edition: InventoryItemApp['edition'] | undefined;\n if (resellerItem.usesEditions && resellerItem.editionId) {\n edition = {\n editionId: resellerItem.editionId,\n name: resellerItem.name ?? '',\n };\n }\n return {\n itemId: resellerItem.appId ?? '',\n type: 'app',\n name: resellerItem.name ?? '',\n iconUrl: resellerItem.icon ?? '',\n edition: edition,\n quantity: 1,\n };\n}\n", "import { Addon as MarketplaceAddon } from '@galaxy/marketplace-apps/v1';\nimport { App as MarketplaceApp, AppType as MarketplaceAppType } from '@vendasta/marketplace-apps';\nimport { Package as MarketplacePackage } from '@vendasta/marketplace-packages';\nimport { InventoryItemAddon, InventoryItemApp, InventoryItemBase, InventoryItemPackage } from '../item-types';\n\nexport function fromMarketplaceApp(mp: MarketplaceApp): InventoryItemApp | InventoryItemAddon {\n const baseItem: InventoryItemBase = {\n itemId: mp.key.appId,\n name: mp.sharedMarketingInformation?.name,\n iconUrl: mp.sharedMarketingInformation?.iconUrl,\n type: 'app',\n quantity: 1,\n };\n\n if (mp.appType === MarketplaceAppType.APP_TYPE_ADDON) {\n return {\n ...baseItem,\n type: 'addon',\n parentItemId: mp.parentRequirements?.parentDetails?.key?.appId,\n };\n }\n\n let edition: InventoryItemApp['edition'];\n const usesEditions = mp.key?.editionId != null;\n if (usesEditions) {\n edition = {\n editionId: mp.key?.editionId ?? '',\n name: mp.sharedMarketingInformation?.editionName ?? '',\n };\n }\n\n return {\n ...baseItem,\n type: 'app',\n edition: edition,\n };\n}\n\nexport function fromMarketplaceAddon(addon: MarketplaceAddon): InventoryItemAddon {\n return {\n itemId: addon.addonId,\n type: 'addon',\n name: addon.productName,\n iconUrl: addon.icon,\n parentItemId: addon.appId,\n quantity: 1,\n };\n}\n\nexport function fromMarketplacePackage(marketplacePackage: MarketplacePackage): InventoryItemPackage {\n return {\n itemId: marketplacePackage.packageId,\n type: 'package',\n name: marketplacePackage.name,\n iconUrl: marketplacePackage.icon,\n quantity: 1,\n };\n}\n", "import { Addon as MarketplaceAddon } from '@galaxy/marketplace-apps/v1';\nimport { App as MarketplaceApp } from '@vendasta/marketplace-apps';\nimport { Package as MarketplacePackage } from '@vendasta/marketplace-packages';\nimport { GenericResellerItem, fromResellerItem } from './store';\nimport { fromMarketplaceAddon, fromMarketplaceApp, fromMarketplacePackage } from './marketplace';\nimport { InventoryItem } from '../type';\n\nexport type InventoryItemAdapter = MarketplaceApp | MarketplaceAddon | MarketplacePackage | GenericResellerItem;\n\nexport function ToInventoryItem(item: T): InventoryItem {\n if (isMarketplaceApp(item)) {\n return fromMarketplaceApp(item);\n }\n\n if (isMarketplaceAddon(item)) {\n return fromMarketplaceAddon(item);\n }\n\n if (isMarketplacePackage(item)) {\n return fromMarketplacePackage(item);\n }\n\n if (isResellerItem(item)) {\n return fromResellerItem(item);\n }\n\n throw new Error('ToInventoryItem error - missing adapter for item:' + item);\n}\n\nexport function isMarketplaceApp(item: InventoryItemAdapter): item is MarketplaceApp {\n return item instanceof MarketplaceApp;\n}\n\nexport function isMarketplaceAddon(item: InventoryItemAdapter): item is MarketplaceAddon {\n return item instanceof MarketplaceAddon;\n}\n\nexport function isMarketplacePackage(item: InventoryItemAdapter): item is MarketplacePackage {\n return item instanceof MarketplacePackage;\n}\n\nexport function isResellerItem(item: InventoryItemAdapter): item is GenericResellerItem {\n return item instanceof GenericResellerItem;\n}\n", "import { ItemType } from '@vendasta/marketplace-apps';\nimport { InventoryItem } from '../../common/inventory-item';\nimport { ItemSelectorRowItem, SelectedItem } from '../interface';\n\nexport function findItem(items: SelectedItem[], itemId: string): [SelectedItem | null, number] {\n const index = items?.findIndex((selectedItem) => {\n return selectedItem.itemId === itemId;\n });\n if (index >= 0) {\n return [items[index], index];\n }\n return [null, -1];\n}\n\nexport function convertInventoryItemToSelectedItem(inventoryItem: InventoryItem, immutable = false): SelectedItem {\n return {\n ...inventoryItem,\n immutable: immutable,\n };\n}\n\nexport function convertRowItemToSelectedItem(rowItem: ItemSelectorRowItem): SelectedItem {\n const name = rowItem.sellerWhitelabelName || rowItem.defaultName;\n const iconUrl = rowItem.sellerWhitelabelIconUrl || rowItem.iconUrl;\n const base: Omit = {\n itemId: rowItem.itemId,\n name: name ?? '',\n iconUrl: iconUrl ?? '',\n quantity: rowItem.quantity ?? 1,\n immutable: rowItem.immutable,\n };\n\n if (rowItem.itemType === ItemType.ITEM_TYPE_PACKAGE) {\n return {\n ...base,\n type: 'package',\n };\n }\n\n if (rowItem.itemType === ItemType.ITEM_TYPE_ADDON) {\n return {\n ...base,\n type: 'addon',\n parentItemId: rowItem?.requiredItems?.[0].itemId ?? '',\n };\n }\n\n const rowEdition = rowItem?.editions?.find((edition) => edition.editionId === rowItem.editionId);\n return {\n ...base,\n type: 'app',\n edition: {\n editionId: rowEdition?.editionId ?? '',\n name: rowEdition?.name ?? '',\n },\n };\n}\n\nexport function mapify(array: I[], keyFn: (item: I) => string, valueFn: (item: I) => V): Map {\n return array.reduce((map, item) => {\n map.set(keyFn(item), valueFn ? valueFn(item) : item);\n return map;\n }, new Map());\n}\n", "import { Edition as MarketplaceEdition } from '@vendasta/marketplace-apps/v1';\nimport { Edition, ItemSelectorRowItem, SelectedItem } from '../interface';\nimport { findItem } from './item';\n\nexport function convertToItemSelectorEdition(marketplaceEdition: MarketplaceEdition): Edition {\n const editionId = marketplaceEdition?.editionId ? marketplaceEdition?.editionId : '';\n return {\n appId: marketplaceEdition?.appId,\n name: marketplaceEdition?.name,\n editionId: editionId,\n };\n}\n\nexport function areEditionIdsEqual(input1: string | null | undefined, input2: string | null | undefined): boolean {\n // Pro editions of O&O products are represented by an empty edition ID\n const editionId1 = input1 ?? '';\n const editionId2 = input2 ?? '';\n return editionId1 === editionId2;\n}\n\nexport function findItemEdition(item: SelectedItem | null, editions: Edition[]): Edition | null {\n if (!editions?.length) return null;\n\n if (item?.type !== 'app' || !item?.edition) {\n return editions[0];\n }\n\n return (\n editions.find((edition) => {\n const nullishMatch = !edition?.editionId && !item.edition?.editionId;\n const stringMatch = edition?.editionId === item.edition?.editionId;\n return nullishMatch || stringMatch;\n }) ?? null\n );\n}\n\nexport function markActiveEdition(\n selectedItems: SelectedItem[],\n immutableSelectedItems: SelectedItem[],\n activatableItem: ItemSelectorRowItem,\n): ItemSelectorRowItem {\n const [selected] = findItem(selectedItems, activatableItem.itemId);\n const [immutableItem] = findItem(immutableSelectedItems, activatableItem.itemId);\n\n if (immutableItem?.type !== 'app' || !immutableItem?.edition) return activatableItem;\n\n const activeEdition = findItemEdition(immutableItem, activatableItem.editions ?? []);\n if (!activeEdition) return activatableItem;\n\n activeEdition.isActive = true;\n if (!selected && immutableItem) {\n // reset the editionId for an active item when it's no longer selected\n activatableItem.editionId = activeEdition.editionId;\n }\n\n return activatableItem;\n}\n", "import { Injectable } from '@angular/core';\nimport { App, AppKey, GetMultiAppRequest, PartnerApiService } from '@vendasta/marketplace-apps';\nimport { MarketplacePackagesApiService, Package } from '@vendasta/marketplace-packages';\nimport { Observable, of } from 'rxjs';\nimport { map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';\nimport { InventoryItem, ToInventoryItem } from '../common/inventory-item';\nimport { areEditionIdsEqual, mapify } from '../item-selector/util';\n\n@Injectable()\nexport class PackageContentsService {\n constructor(\n private readonly marketplacePackagesApiService: MarketplacePackagesApiService,\n private readonly partnerApiService: PartnerApiService,\n ) {}\n\n fetchPackageItems(packageId: string, partnerId: string, marketId: string): Observable {\n return this.fetchMultiPackageItems([packageId], partnerId, marketId).pipe(\n map((packageItemsMap) => packageItemsMap.get(packageId) ?? []),\n );\n }\n\n fetchMultiPackageItems(\n packageIds: string[],\n partnerId: string,\n marketId: string,\n ): Observable> {\n if (!packageIds.length) {\n return of(new Map());\n }\n\n const packages$ = this.getPackages(packageIds);\n const lineItems$ = packages$.pipe(switchMap((packages) => this.getLineItems(packages, partnerId, marketId)));\n return lineItems$.pipe(\n withLatestFrom(packages$),\n map(([lineItems, packages]) => {\n const packageItemsMap = mapify(\n packages,\n (p) => p.packageId,\n (p) => this.transformPackageLineItems(p, lineItems),\n );\n return packageItemsMap;\n }),\n );\n }\n\n private getPackages(packageIds: string[]): Observable {\n return this.marketplacePackagesApiService.getMultiPackages({ packageIds: packageIds }).pipe(\n map((resp) => resp.packageContainers.map((p) => p.package)),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n private getLineItems(packages: Package[], partnerId: string, marketId: string): Observable {\n const getMultiAppRequest = new GetMultiAppRequest({\n projectionFilter: {\n paths: ['editionInformation', 'sharedMarketingInformation'],\n },\n appKeys: packages\n .map((p) => p.lineItems.lineItems)\n .flat()\n .map((lineItem) => {\n return new AppKey({\n appId: lineItem.id,\n editionId: lineItem.editionId || '',\n });\n }),\n partnerId: partnerId,\n marketId: marketId,\n });\n return this.partnerApiService.getMultiApp(getMultiAppRequest).pipe(\n map((resp) => resp?.apps ?? []),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n private transformPackageLineItems(packageItem: Package, lineItems: App[]): InventoryItem[] {\n const packageLineItems = packageItem.lineItems.lineItems;\n const transformedLineItems = packageLineItems.map((pli) => {\n const lineItem = lineItems.find(\n (li) => li.key.appId === pli.id && areEditionIdsEqual(li.key.editionId, pli.editionId),\n )!;\n return { ...ToInventoryItem(lineItem), quantity: pli?.quantity ?? 1 };\n });\n return transformedLineItems;\n }\n}\n", "import { Component, Input } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { PopoverPositions } from '@vendasta/galaxy/popover';\nimport { SelectedItem } from '../item-selector/interface';\nimport { PackageContentsService } from '../services/package-contents.service';\n\n@Component({\n selector: 'inventory-ui-package-contents-popover',\n templateUrl: './package-contents-popover.component.html',\n styleUrls: ['./package-contents-popover.component.scss'],\n})\nexport class PackageContentsPopoverComponent {\n protected readonly PopoverPositions = PopoverPositions;\n\n @Input() packageId = '';\n @Input() partnerId = '';\n @Input() marketId = '';\n\n packageItems$!: Observable;\n isOpen = false;\n\n constructor(private readonly packageContentsService: PackageContentsService) {}\n\n togglePopover(): void {\n if (!this.isOpen && !this.packageItems$) {\n this.packageItems$ = this.packageContentsService.fetchPackageItems(this.packageId, this.partnerId, this.marketId);\n }\n this.isOpen = !this.isOpen;\n }\n}\n", "\n Package (view contents)\n\n\n\n
\n \n \n \n
\n\n\n\n \n\n", "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { GalaxyPopoverModule } from '@vendasta/galaxy/popover';\nimport { GalaxyLoadingSpinnerModule } from '@vendasta/galaxy/loading-spinner';\nimport { PackageContentsPopoverComponent } from './package-contents-popover.component';\nimport { PackageContentsService } from '../services/package-contents.service';\nimport { ItemCommonModule } from '../common/item-common.module';\n\n@NgModule({\n imports: [CommonModule, GalaxyPopoverModule, GalaxyLoadingSpinnerModule, ItemCommonModule],\n exports: [PackageContentsPopoverComponent],\n declarations: [PackageContentsPopoverComponent],\n providers: [PackageContentsService],\n})\nexport class PackageContentsPopoverModule {}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { Frequency as GalaxyFrequency } from '@vendasta/galaxy/frequency';\nimport { RevenuePeriod } from '@vendasta/sales-orders';\n\n@Pipe({ name: 'revenuePeriodToGalaxyFrequency', standalone: true })\nexport class RevenuePeriodToGalaxyFrequency implements PipeTransform {\n transform(value: RevenuePeriod): GalaxyFrequency {\n return revenuePeriodToGalaxyFrequency(value);\n }\n}\n\nexport function revenuePeriodToGalaxyFrequency(value: RevenuePeriod): GalaxyFrequency {\n switch (value) {\n case RevenuePeriod.ONETIME:\n return GalaxyFrequency.ONE_TIME;\n case RevenuePeriod.YEARLY:\n return GalaxyFrequency.YEARLY;\n default:\n return GalaxyFrequency.MONTHLY;\n }\n}\n\n@Pipe({ name: 'galaxyFrequencyToRevenuePeriod', standalone: true })\nexport class GalaxyFrequencyToRevenuePeriodPipe implements PipeTransform {\n transform(value: GalaxyFrequency): RevenuePeriod {\n return galaxyFrequencyToRevenuePeriod(value);\n }\n}\n\nexport function galaxyFrequencyToRevenuePeriod(value: GalaxyFrequency): RevenuePeriod {\n switch (value) {\n case GalaxyFrequency.ONE_TIME:\n return RevenuePeriod.ONETIME;\n case GalaxyFrequency.YEARLY:\n return RevenuePeriod.YEARLY;\n default:\n return RevenuePeriod.MONTHLY;\n }\n}\n", "import { AppPrices, FrequencyPrice, PriceDisplayOverride, UnitBillingFrequency } from '@vendasta/marketplace-apps';\nimport { ItemBasePrice } from './base';\nimport { ItemFrequencyPrice, calculateUnitPriceFromInstancePrice, getFrequencyPrices } from './frequency';\nimport { getPartnerCurrency, toGalaxyFrequency } from '@vendasta/billing-ui';\nimport { InventoryItemWithPricing } from '../inventory-item';\nimport { revenuePeriodToGalaxyFrequency } from '../../billing-term-dialog/sales-to-galaxy-frequency.pipe';\nimport { RevenuePeriod } from '@vendasta/sales-orders';\n\nexport type ItemRetailPrice = ItemBasePrice & ItemFrequencyPrice;\n\nexport type ParseRetailPriceConfig = {\n appPrices: AppPrices;\n item: InventoryItemWithPricing;\n marketCurrency: string;\n};\n\nexport function parseRetailItemPrice({ appPrices, item, marketCurrency }: ParseRetailPriceConfig): ItemRetailPrice {\n const editionId = item.type === 'app' ? item.edition?.editionId : undefined;\n const appPrice = appPrices?.pricesForContexts?.['retail'];\n const unitPrice = appPrice?.pricesForEditions?.[editionId ?? ''];\n let currency = getPartnerCurrency(appPrice);\n\n let frequencyPrices = getFrequencyPrices(appPrices, editionId, 'retail');\n if (!frequencyPrices?.length) {\n // fallback to wholesale pricing if retail is unavailable,\n frequencyPrices = getFrequencyPrices(appPrices, editionId, 'wholesale');\n currency = marketCurrency;\n }\n\n // retail price is limited to one frequency\n const retailPrice = parseRetailFrequencyPrice({\n mpFrequencyPrice: frequencyPrices[0],\n item,\n });\n\n return {\n ...retailPrice,\n currency: currency,\n contactSales: appPrice?.displayOverride === PriceDisplayOverride.PRICE_DISPLAY_OVERRIDE_CONTACT_SALES,\n discounts: unitPrice?.discount ?? [],\n };\n}\n\ntype parseRetailFrequencyPriceConfig = {\n mpFrequencyPrice: FrequencyPrice;\n item: InventoryItemWithPricing;\n};\n\nfunction parseRetailFrequencyPrice({\n mpFrequencyPrice,\n item: item,\n}: parseRetailFrequencyPriceConfig): ItemFrequencyPrice {\n const minimumPrice = mpFrequencyPrice?.pricingRules?.[0].price || 0;\n let price = minimumPrice;\n let frequency: ItemFrequencyPrice['frequency'] = toGalaxyFrequency(\n mpFrequencyPrice?.frequency ?? UnitBillingFrequency.UNIT_BILLING_FREQUENCY_ONETIME,\n );\n\n const hasInstancePrice = Boolean(mpFrequencyPrice?.instancePrice);\n if (hasInstancePrice) {\n price = calculateUnitPriceFromInstancePrice(\n mpFrequencyPrice.instancePrice,\n mpFrequencyPrice.setupFee,\n item.quantity,\n );\n }\n\n const revenueComponent = item?.pricing?.currentRevenue?.revenueComponents?.[0];\n if (revenueComponent) {\n frequency = revenuePeriodToGalaxyFrequency(revenueComponent.period ?? RevenuePeriod.ONETIME);\n price = revenueComponent.value ?? 0;\n }\n\n return {\n price: price,\n total: price * item.quantity,\n frequency: frequency,\n minimumPrice: minimumPrice,\n };\n}\n", "import { getPartnerCurrency, toGalaxyFrequency } from '@vendasta/billing-ui';\nimport { AppPrices, FrequencyPrice, PriceDisplayOverride, UnitBillingFrequency } from '@vendasta/marketplace-apps';\nimport { ItemBasePrice } from './base';\nimport { ItemFrequencyPrice, calculateUnitPriceFromInstancePrice, getFrequencyPrices } from './frequency';\nimport { InventoryItemWithPricing } from '../inventory-item';\n\nexport type ItemWholesalePrice = ItemBasePrice & {\n frequencyPrices: ItemFrequencyPrice[];\n usesVariablePricing?: boolean;\n contactSales?: boolean;\n};\n\nexport type ParseWholesalePriceConfig = {\n appPrices: AppPrices;\n item: InventoryItemWithPricing;\n};\n\nexport function parseWholesaleItemPrice({ appPrices, item }: ParseWholesalePriceConfig): ItemWholesalePrice {\n const editionId = item.type === 'app' ? item.edition?.editionId : undefined;\n const appPrice = appPrices?.pricesForContexts?.['wholesale'];\n const unitPrice = appPrice?.pricesForEditions?.[editionId ?? ''];\n const marketplaceFrequencyPrices = getFrequencyPrices(appPrices, editionId, 'wholesale');\n\n const usesVariablePricing = appPrice?.billingConfiguration?.usesVariablePricing;\n\n return {\n frequencyPrices: parseWholesaleFrequencyPrices({\n marketplaceFrequencyPrices: marketplaceFrequencyPrices,\n item: item,\n usesVariablePricing,\n }),\n currency: getPartnerCurrency(appPrice),\n usesVariablePricing: appPrice?.billingConfiguration?.usesVariablePricing,\n contactSales: appPrice?.displayOverride === PriceDisplayOverride.PRICE_DISPLAY_OVERRIDE_CONTACT_SALES,\n discounts: unitPrice?.discount ?? [],\n };\n}\n\ntype parseWholesaleFrequencyPrices = {\n marketplaceFrequencyPrices: FrequencyPrice[];\n item: InventoryItemWithPricing;\n usesVariablePricing: boolean;\n};\n\nfunction parseWholesaleFrequencyPrices({\n marketplaceFrequencyPrices,\n item: item,\n usesVariablePricing,\n}: parseWholesaleFrequencyPrices): ItemFrequencyPrice[] {\n return marketplaceFrequencyPrices.map((fp) => {\n const minimumPrice = fp?.pricingRules?.[0].price || 0;\n let price = minimumPrice;\n\n if (usesVariablePricing && item.pricing?.cost?.customPrice) {\n price = item.pricing.cost.customPrice;\n }\n\n const hasInstancePrice = Boolean(fp?.instancePrice);\n if (hasInstancePrice && !usesVariablePricing) {\n price = calculateUnitPriceFromInstancePrice(fp.instancePrice, fp.setupFee, item.quantity);\n }\n\n return {\n price: price,\n total: price * item.quantity,\n frequency: toGalaxyFrequency(fp?.frequency ?? UnitBillingFrequency.UNIT_BILLING_FREQUENCY_ONETIME),\n minimumPrice: minimumPrice,\n };\n });\n}\n", "import { Frequency as GalaxyFrequency } from '@vendasta/galaxy/frequency';\nimport { InventoryItemWithPricing } from '../common/inventory-item';\nimport { ItemRetailPrice, ItemWholesalePrice } from '../common/item-price';\n\nexport type ItemRow = InventoryItemWithPricing & {\n wholesalePricing: ItemWholesalePrice;\n retailPricing: ItemRetailPrice;\n fees: ItemFeeRow[];\n isMultiactivatable?: boolean;\n retailChangeConfirmed?: boolean;\n billingTerm?: BillingTerm;\n index?: number;\n};\n\nexport interface ItemFeeRow {\n type: 'setup' | 'management';\n label: string;\n amountType: 'fixed' | 'percentage';\n retailAmount?: number;\n wholesaleAmount: number;\n retailAmountTotal?: number;\n frequency: GalaxyFrequency;\n}\n\nexport interface PriceSummary {\n oneTime: number;\n monthly: number;\n yearly: number;\n currency: string;\n}\n\nexport interface PriceTotal {\n wholesaleTotal: PriceSummary;\n retailTotal: PriceSummary;\n initialized: boolean; // controls the loading state of the summary component\n}\n\nexport interface BillingTerm {\n startDate?: Date;\n endDate?: Date;\n}\n\nexport interface ItemRowChangeEvent {\n item: ItemRow;\n action: ItemRowChangeEventAction;\n}\n\nexport type ItemRowChangeEventAction = 'add' | 'update' | 'delete' | 'add-billing-term' | 'edit-billing-term';\n\nexport const WHOLESALE_PRICING_CONTEXT = 'wholesale';\nexport const RETAIL_PRICING_CONTEXT = 'retail';\n", "import { MerchantService } from '@galaxy/billing';\nimport { PagedListRequestInterface, PagedResponseInterface, PaginatedAPIInterface } from '@vendasta/galaxy/table';\nimport {\n App,\n AppKey,\n AppPrices,\n AppPricingApiService,\n FieldMask,\n GetMultiAppRequest,\n GetMultiPricingRequest,\n PartnerApiService,\n RequestedApp,\n} from '@vendasta/marketplace-apps';\nimport { MarketplacePackagesApiService, Package } from '@vendasta/marketplace-packages';\nimport {\n BehaviorSubject,\n Observable,\n combineLatest,\n forkJoin,\n map,\n of,\n skipWhile,\n startWith,\n switchMap,\n take,\n withLatestFrom,\n} from 'rxjs';\nimport { InventoryItemWithPricing } from '../common/inventory-item';\nimport { parseRetailItemPrice } from '../common/item-price/retail';\nimport { parseWholesaleItemPrice } from '../common/item-price/wholesale';\nimport { ItemRow, RETAIL_PRICING_CONTEXT, WHOLESALE_PRICING_CONTEXT } from './item-pricing-table.interface';\nimport { parseItemFees } from './item-pricing-table.utils';\n\nexport class ItemPricingTableDataSource implements PaginatedAPIInterface {\n private refreshTrigger$$: BehaviorSubject = new BehaviorSubject(undefined);\n\n private appCache: Map = new Map();\n private packageCache: Map = new Map();\n private appPricesCache: Map = new Map();\n\n constructor(\n private readonly items$: Observable,\n private readonly partnerId: string,\n private readonly marketId: string,\n private readonly businessId: string,\n private readonly appPricingApiService: AppPricingApiService,\n private readonly partnerApiService: PartnerApiService,\n private readonly merchantService: MerchantService,\n private readonly marketplacePackagesApiService: MarketplacePackagesApiService,\n ) {}\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n get(req: PagedListRequestInterface): Observable> {\n const rows$ = this.items$.pipe(\n skipWhile((items) => !items?.length),\n map((items) => {\n const pricesToFetch = items.filter((item) => !this.appPricesCache.has(item.itemId));\n const appsToFetch = items.filter((item) => item.type !== 'package' && !this.appCache.has(item.itemId));\n const packagesToFetch = items.filter((item) => item.type === 'package' && !this.packageCache.has(item.itemId));\n return [items, pricesToFetch, appsToFetch, packagesToFetch];\n }),\n switchMap(([items, pricesToFetch, appsToFetch, packagesToFetch]) => {\n const appPrices$ = this.fetchAppPrices(pricesToFetch);\n const apps$ = this.fetchApps(appsToFetch);\n const packages$ = this.fetchPackages(packagesToFetch);\n return forkJoin([of(items), appPrices$, apps$, packages$]);\n }),\n map(([items, prices, apps, packages]) => {\n this.appPricesCache = new Map([...this.appPricesCache, ...prices]);\n this.appCache = new Map([...this.appCache, ...apps]);\n this.packageCache = new Map([...this.packageCache, ...packages]);\n return { items, prices: this.appPricesCache, apps: this.appCache, packages: this.packageCache };\n }),\n withLatestFrom(this.getMarketCurrency$()),\n map(([{ items, prices, apps, packages }, marketCurrency]) => {\n const rows = this.buildRows(items, prices, apps, packages, marketCurrency);\n return {\n data: rows,\n pagingMetadata: {\n nextCursor: '',\n hasMore: false,\n },\n } as PagedResponseInterface;\n }),\n );\n\n return combineLatest([rows$, this.refreshTrigger$$]).pipe(\n map(([rows]) => {\n rows.data = rows.data.filter((row) => row.quantity > 0);\n return rows;\n }),\n startWith({ data: [], pagingMetadata: { nextCursor: '', hasMore: false } }),\n );\n }\n private buildRows(\n items: InventoryItemWithPricing[],\n itemPrices: Map,\n apps: Map,\n packages: Map,\n marketCurrency: string,\n ) {\n return items.map((item, index) => {\n const itemPrice = itemPrices.get(item.itemId) as AppPrices;\n const app = apps.get(item.itemId) as App;\n const pkg = packages.get(item.itemId) as Package;\n return this.buildItemRow(item, itemPrice, app, pkg, marketCurrency, index);\n });\n }\n\n private fetchAppPrices(items: InventoryItemWithPricing[]): Observable> {\n if (!items.length) return of(new Map());\n const getMultiPricingRequest = new GetMultiPricingRequest({\n appIds: items.map((item) => item.itemId),\n pricingContexts: {\n paths: [WHOLESALE_PRICING_CONTEXT, RETAIL_PRICING_CONTEXT],\n },\n partnerId: this.partnerId,\n marketId: this.marketId,\n filterOptions: new FieldMask({ paths: ['instance_price'] }),\n requestedItems: items.map((item) => {\n return new RequestedApp({\n appId: item.itemId,\n quantity: item.quantity,\n });\n }),\n });\n return this.appPricingApiService\n .getMultiPricing(getMultiPricingRequest)\n .pipe(map((response) => this.mapify(response.appPrices, (ap: AppPrices) => ap.appId)));\n }\n\n private fetchApps(items: InventoryItemWithPricing[]): Observable> {\n const appItems = items.filter((item) => item.type !== 'package');\n if (!appItems.length) return of(new Map());\n\n const getMultiAppRequest = new GetMultiAppRequest({\n projectionFilter: {\n paths: ['sharedMarketingInformation'],\n },\n appKeys: appItems.map((item) => {\n return new AppKey({\n appId: item.itemId,\n editionId: item.type === 'app' ? item.edition?.editionId : undefined,\n });\n }),\n partnerId: this.partnerId,\n marketId: this.marketId,\n });\n\n return this.partnerApiService\n .getMultiApp(getMultiAppRequest)\n .pipe(map((response) => this.mapify(response.apps, (a: App) => a.key?.appId)));\n }\n\n private fetchPackages(items: InventoryItemWithPricing[]): Observable> {\n const packageIds = items.filter((item) => item.type === 'package').map((item) => item.itemId);\n if (!packageIds.length) return of(new Map());\n return this.marketplacePackagesApiService.getMultiPackages({ packageIds: packageIds }).pipe(\n map((resp) => {\n const packages = resp.packageContainers.map((container) => container.package);\n return this.mapify(packages, (p: Package) => p.packageId);\n }),\n );\n }\n\n private getMarketCurrency$() {\n return this.merchantService.getMultiRetailConfigurations(this.partnerId, [this.marketId]).pipe(\n take(1),\n map((resp) => {\n let currencyCode = '';\n resp.forEach((retailConfig) => {\n if (retailConfig?.currencyCode) {\n currencyCode = retailConfig.currencyCode;\n }\n });\n return currencyCode;\n }),\n );\n }\n\n private buildItemRow(\n item: InventoryItemWithPricing,\n appPrices: AppPrices,\n app: App,\n marketplacePackage: Package,\n marketCurrency: string,\n index: number,\n ): ItemRow {\n if (item?.pricing?.quantity) {\n item.quantity = item.pricing.quantity;\n }\n const itemRow = {\n ...item,\n name: item.type !== 'package' ? app?.sharedMarketingInformation?.name : marketplacePackage?.name,\n iconUrl: item.type !== 'package' ? app?.sharedMarketingInformation?.iconUrl : marketplacePackage?.icon,\n wholesalePricing: parseWholesaleItemPrice({\n appPrices,\n item,\n }),\n retailPricing: parseRetailItemPrice({\n appPrices,\n item,\n marketCurrency,\n }),\n fees: parseItemFees(appPrices, item),\n isMultiactivatable: app?.activationInformation?.multipleActivationsEnabled || false,\n retailChangeConfirmed: false,\n index: index,\n } as ItemRow;\n if (itemRow.type === 'app' && itemRow.edition) {\n itemRow.edition.name = app?.sharedMarketingInformation?.editionName;\n }\n if (item.pricing?.billingPeriod) {\n itemRow.billingTerm = {\n startDate: item.pricing.billingPeriod.startDate,\n endDate: item.pricing.billingPeriod.endDate,\n };\n }\n return itemRow;\n }\n\n refreshItemRows(): void {\n this.refreshTrigger$$.next();\n }\n\n private mapify(array: any[], keyFn: (item: any) => string): Map {\n return array.reduce((map, item) => {\n map.set(keyFn(item), item);\n return map;\n }, new Map());\n }\n}\n", "import { MatDialogRef } from '@angular/material/dialog';\nimport { Component } from '@angular/core';\n\n@Component({\n selector: 'inventory-ui-multi-activatable-dialog',\n templateUrl: './multi-activatable-dialog.component.html',\n})\nexport class MultiActivatableDialogComponent {\n constructor(public dialogRef: MatDialogRef) {}\n}\n", "

Change price?

\n\n

\n You previously sold this item to this customer for a different price. Changing this price will also update each of\n this customer's existing subscriptions for this item\n

\n
\n\n \n \n\n", "import { Pipe, PipeTransform } from '@angular/core';\n\n@Pipe({ name: 'formatWholesaleMinimumHint' })\nexport class FormatWholesaleMinimumHintPipe implements PipeTransform {\n transform(value: string): string {\n return `Minimum: ${value}`;\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { ItemFeeRow } from '../item-pricing-table.interface';\n\n@Pipe({ name: 'hasSetupFee', pure: false })\nexport class HasSetupFeePipe implements PipeTransform {\n transform(itemFees: ItemFeeRow[]): boolean {\n return itemFees.some((fee) => fee.type === 'setup');\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { ItemFeeRow } from '../item-pricing-table.interface';\n\n@Pipe({ name: 'isSetupFeeRemovable', pure: false })\nexport class IsSetupFeeRemovablePipe implements PipeTransform {\n transform(fee: ItemFeeRow): boolean {\n return fee.type === 'setup' && (fee.retailAmount ?? 0) > 0 && !fee.wholesaleAmount;\n }\n}\n", "import { inject, Pipe, PipeTransform } from '@angular/core';\nimport { Frequency as GalaxyFrequency } from '@vendasta/galaxy/frequency';\nimport { BillingTerm } from '../item-pricing-table.interface';\nimport { FormatBillingPeriodPipe } from '../../common/pipes/billing-period.pipe';\nimport { galaxyFrequencyToRevenuePeriod } from '../../billing-term-dialog/sales-to-galaxy-frequency.pipe';\n\n@Pipe({ name: 'formatBillingTerm' })\nexport class FormatBillingTermPipe implements PipeTransform {\n private formatBillingPeriodPipe = inject(FormatBillingPeriodPipe);\n\n transform(billingTerm: BillingTerm, frequency?: GalaxyFrequency): string {\n if (!frequency) {\n return this.formatBillingPeriodPipe.transform(billingTerm);\n }\n\n const revenuePeriod = galaxyFrequencyToRevenuePeriod(frequency);\n\n return this.formatBillingPeriodPipe.transform(billingTerm, revenuePeriod);\n }\n}\n", "import { Component, DestroyRef, EventEmitter, Injector, Input, OnInit, Output, Signal, inject } from '@angular/core';\nimport { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';\nimport { MatDialog } from '@angular/material/dialog';\nimport { MerchantService } from '@galaxy/billing';\nimport { Frequency } from '@vendasta/galaxy/frequency';\nimport { GalaxyDataSource } from '@vendasta/galaxy/table';\nimport { GalaxyColumnDef } from '@vendasta/galaxy/table/src/table.interface';\nimport { AppPricingApiService, GetMultiPricingRequest, PartnerApiService } from '@vendasta/marketplace-apps';\nimport { MarketplacePackagesApiService } from '@vendasta/marketplace-packages';\nimport { BehaviorSubject, Observable, combineLatest, filter, of, startWith, take } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { InventoryItemWithPricing } from '../common/inventory-item';\nimport { parseRetailItemPrice } from '../common/item-price/retail';\nimport { parseWholesaleItemPrice } from '../common/item-price/wholesale';\nimport { ItemPricingTableDataSource } from './item-pricing-table.data-source';\nimport {\n ItemFeeRow,\n ItemRow,\n ItemRowChangeEvent,\n ItemRowChangeEventAction,\n PriceSummary,\n PriceTotal,\n RETAIL_PRICING_CONTEXT,\n WHOLESALE_PRICING_CONTEXT,\n} from './item-pricing-table.interface';\nimport {\n calculateTotalRetailPrice,\n calculateTotalWholesalePrice,\n convertHTMLControlValueToNumber,\n parseItemFees,\n updatePercentageManagementFee,\n} from './item-pricing-table.utils';\nimport { MultiActivatableDialogComponent } from './multi-activatable-dialog/multi-activatable-dialog.component';\n\nexport interface ItemPricingTableConfig {\n partnerId: string;\n marketId: string;\n businessId?: string;\n selectionEditable?: boolean;\n quantityEditable?: boolean;\n billingTermsEnabled?: boolean;\n hideColumns?: ItemPricingTableColumnID[];\n hidePriceSummary?: boolean;\n hideBorder?: boolean;\n}\n\nexport type ItemPricingTableColumnID =\n | 'name'\n | 'quantity'\n | 'wholesale_price'\n | 'retail_price'\n | 'retail_frequency'\n | 'retail_price_total'\n | 'menu';\n\nconst ITEM_PRICING_TABLE_COLUMNS: GalaxyColumnDef[] = [\n {\n id: 'name',\n title: 'Name',\n },\n {\n id: 'quantity',\n title: 'Quantity',\n },\n {\n id: 'wholesale_price',\n title: 'Wholesale',\n },\n {\n id: 'retail_price',\n title: 'Retail',\n },\n {\n id: 'retail_frequency',\n title: 'Frequency',\n },\n {\n id: 'retail_price_total',\n title: 'Retail Total',\n },\n {\n id: 'menu',\n title: 'Menu',\n },\n];\n\n@Component({\n selector: 'inventory-ui-item-pricing-table',\n templateUrl: './item-pricing-table.component.html',\n styleUrls: ['./item-pricing-table.component.scss'],\n})\nexport class ItemPricingTableComponent implements OnInit {\n @Input() config!: ItemPricingTableConfig;\n protected _items$$ = new BehaviorSubject([]);\n @Input() set items(value: InventoryItemWithPricing[]) {\n this._items$$.next(value);\n }\n\n @Output() updatedItemRow = new EventEmitter();\n @Output() valid = new EventEmitter();\n @Output() wholesaleTotal = new EventEmitter();\n\n private _invalidPriceItems: string[] = [];\n\n dataSource!: ItemPricingTableDataSource;\n tableDataSource!: GalaxyDataSource;\n columns: GalaxyColumnDef[] = ITEM_PRICING_TABLE_COLUMNS;\n Frequency = Frequency;\n priceTotals$!: Observable;\n showManagementFees = true;\n showRetailFrequencySuffix = false;\n\n private injector = inject(Injector);\n private dataMembers!: Signal;\n private wholesaleSummary$!: Observable;\n private retailSummary$!: Observable;\n public wholesaleSummary!: Signal;\n public retailSummary!: Signal;\n\n constructor(\n private dialog: MatDialog,\n private readonly partnerApiService: PartnerApiService,\n private readonly appPricingApiService: AppPricingApiService,\n private readonly merchantService: MerchantService,\n private readonly marketplacePackagesApiService: MarketplacePackagesApiService,\n private readonly destroyRef: DestroyRef,\n ) {}\n\n ngOnInit(): void {\n if (this.config?.hideColumns?.length) {\n this.config.hideColumns.forEach((col) => {\n if (col === 'wholesale_price') {\n this.showManagementFees = false;\n }\n if (col === 'retail_frequency') {\n this.showRetailFrequencySuffix = true;\n }\n\n this.columns = this.columns.filter((c) => c.id !== col);\n });\n }\n\n this.dataSource = new ItemPricingTableDataSource(\n this._items$$.asObservable(),\n this.config?.partnerId,\n this.config?.marketId,\n this.config?.businessId ?? '',\n this.appPricingApiService,\n this.partnerApiService,\n this.merchantService,\n this.marketplacePackagesApiService,\n );\n this.tableDataSource = new GalaxyDataSource(this.dataSource);\n this.dataMembers = toSignal(this.tableDataSource.dataMembers$, {\n initialValue: [],\n injector: this.injector,\n });\n\n this.wholesaleSummary$ = this.getPriceSummaryByContext('wholesale');\n this.retailSummary$ = this.getPriceSummaryByContext('retail');\n this.wholesaleSummary = toSignal(this.wholesaleSummary$, {\n initialValue: { oneTime: 0, monthly: 0, yearly: 0, currency: '' },\n injector: this.injector,\n });\n this.retailSummary = toSignal(this.retailSummary$, {\n initialValue: { oneTime: 0, monthly: 0, yearly: 0, currency: '' },\n injector: this.injector,\n });\n this.priceTotals$ = combineLatest([this.wholesaleSummary$, this.retailSummary$]).pipe(\n map(([wholesale, retail]) => {\n return {\n wholesaleTotal: wholesale,\n retailTotal: retail,\n initialized: true,\n };\n }),\n startWith({\n wholesaleTotal: { oneTime: 0, monthly: 0, yearly: 0, currency: '' },\n retailTotal: { oneTime: 0, monthly: 0, yearly: 0, currency: '' },\n initialized: false,\n }),\n );\n\n // TODO - clean this up when the ordering workflow is refactored to initialize package pricing\n this.tableDataSource.dataMembers$\n .pipe(\n filter((rows: ItemRow[]) => rows.length > 0),\n take(1),\n )\n .subscribe((rows) => {\n rows.forEach((row) => {\n // explicitly publish changes for packages to initialize pricing\n this.publishChanges(row);\n });\n });\n this.wholesaleSummary$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((wholesale) => {\n this.wholesaleTotal.emit(wholesale.oneTime);\n });\n }\n\n onQuantityChange(changeEvent: Event, item: ItemRow): void {\n const htmlElement = changeEvent.target as HTMLInputElement;\n let quantity = htmlElement.valueAsNumber;\n if (quantity < 1) {\n htmlElement.value = '1';\n quantity = 1;\n }\n this.updateQuantity(item, quantity);\n }\n\n updateQuantity(item: ItemRow, quantity: number): void {\n item.quantity = quantity;\n if (quantity > 0) {\n item.wholesalePricing.frequencyPrices.forEach((fp) => (fp.total = fp.price * item.quantity));\n item.retailPricing.total = item.retailPricing.price * item.quantity;\n }\n const eventType = quantity > 0 ? 'update' : 'delete';\n this.publishChanges(item, eventType);\n }\n\n updateRetailPricing(item: ItemRow, value: number) {\n item.retailPricing.price = value;\n item.retailPricing.total = value * item.quantity;\n }\n\n updateWholesalePricing(item: ItemRow, frequency: Frequency, value: number) {\n const frequencyPrice = item.wholesalePricing.frequencyPrices.find((fp) => fp.frequency === frequency);\n if (!frequencyPrice) return;\n\n const oldWholeSalePrice = frequencyPrice.price;\n frequencyPrice.price = value;\n frequencyPrice.total = value * item.quantity;\n updatePercentageManagementFee(item, oldWholeSalePrice);\n this.validateWholesalePrice(item);\n }\n\n updateSetupFee(item: ItemRow, value: number) {\n item.fees.forEach((fee) => {\n if (fee.type === 'setup') {\n fee.retailAmount = value;\n fee.retailAmountTotal = fee.retailAmount * item.quantity;\n }\n });\n }\n\n validateWholesalePrice(item: ItemRow) {\n const invalidPrice = item.wholesalePricing.frequencyPrices.every(\n (fp) => fp.minimumPrice && fp.minimumPrice >= fp.price,\n );\n if (invalidPrice) {\n if (!this._invalidPriceItems.includes(item.itemId)) {\n this._invalidPriceItems.push(item.itemId);\n this.valid.emit(this._invalidPriceItems.length === 0);\n }\n } else {\n if (this._invalidPriceItems.includes(item.itemId)) {\n this._invalidPriceItems.splice(this._invalidPriceItems.indexOf(item.itemId), 1);\n this.valid.emit(this._invalidPriceItems.length === 0);\n }\n }\n }\n\n resetItemPricing(item: ItemRow): void {\n const request = new GetMultiPricingRequest({\n appIds: [item.itemId],\n pricingContexts: {\n paths: [WHOLESALE_PRICING_CONTEXT, RETAIL_PRICING_CONTEXT],\n },\n partnerId: this.config?.partnerId,\n marketId: this.config?.marketId,\n });\n this.appPricingApiService\n .getMultiPricing(request)\n .pipe(\n take(1),\n map((response) => response.appPrices[0]),\n )\n .subscribe((appPrices) => {\n // exclude custom pricing to make sure marketplace prices are applied\n const itemWithoutPricing: ItemRow = { ...item, pricing: undefined };\n item.wholesalePricing = parseWholesaleItemPrice({ appPrices, item: itemWithoutPricing });\n item.retailPricing = parseRetailItemPrice({\n appPrices,\n item: itemWithoutPricing,\n marketCurrency: item.retailPricing.currency,\n });\n item.fees = parseItemFees(appPrices, itemWithoutPricing);\n this.publishChanges(item);\n });\n }\n\n onPriceChange(\n changeEvent: Event,\n item: ItemRow,\n priceType: 'wholesale' | 'retail' | 'setupfee',\n frequency: Frequency = Frequency.ONE_TIME,\n ): void {\n const htmlInputValue = convertHTMLControlValueToNumber((changeEvent.target).value);\n const price = this.convertDollarsToCents(htmlInputValue);\n if (priceType === 'wholesale') this.updateWholesalePricing(item, frequency, price);\n if (priceType === 'retail') this.updateRetailPricing(item, price);\n if (priceType === 'setupfee') this.updateSetupFee(item, price);\n this.publishChanges(item);\n\n if (priceType !== 'wholesale') {\n this.confirmRetailChange(item)\n .pipe(take(1))\n .subscribe((changesApproved: boolean) => {\n item.retailChangeConfirmed = true;\n if (!changesApproved) {\n this.resetItemPricing(item);\n }\n });\n }\n }\n\n handleFrequencyChange(frequency: Frequency, item: ItemRow): void {\n item.retailPricing.frequency = frequency;\n this.publishChanges(item);\n }\n\n confirmRetailChange(item: ItemRow): Observable {\n if (item.isMultiactivatable && !item.retailChangeConfirmed) {\n return this.dialog\n .open(MultiActivatableDialogComponent, {\n width: '400px',\n })\n .afterClosed()\n .pipe(map(Boolean));\n }\n return of(true);\n }\n\n convertDollarsToCents(value: number): number {\n return Math.round(value * 100);\n }\n\n removeSetupFee(item: ItemRow, fee: ItemFeeRow): void {\n this.updateSetupFee(item, 0);\n item.fees.splice(item.fees.indexOf(fee), 1);\n this.publishChanges(item);\n }\n\n addSetupFee(itemToUpdate: ItemRow): void {\n itemToUpdate.fees.unshift({\n retailAmount: 0,\n amountType: 'fixed',\n label: 'Setup fee',\n type: 'setup',\n wholesaleAmount: 0,\n frequency: Frequency.ONE_TIME,\n });\n }\n\n publishChanges(item: ItemRow, action: ItemRowChangeEventAction = 'update') {\n this.updatedItemRow.emit({ item, action });\n this.dataSource.refreshItemRows();\n }\n\n getPriceSummaryByContext = (type: 'wholesale' | 'retail'): Observable => {\n return this.tableDataSource.dataMembers$.pipe(\n filter((dataMembers) => dataMembers.length > 0),\n map((dataMembers) => {\n if (type === 'wholesale') return calculateTotalWholesalePrice(dataMembers);\n return calculateTotalRetailPrice(dataMembers);\n }),\n startWith({ oneTime: 0, monthly: 0, yearly: 0, currency: '' }),\n );\n };\n\n onRemoveItem(item: ItemRow): void {\n this.updateQuantity(item, 0);\n }\n\n getItems(): ItemRow[] {\n return this.dataMembers();\n }\n}\n", "\n \n \n \n \n {{ 'ITEM_PRICING_TABLE.COLUMNS.ITEM' | translate }}\n \n \n
\n \n \n \n
\n
\n
\n \n subdirectory_arrow_right\n {{ fee.label }}\n \n
\n
\n
\n
\n\n \n \n {{ 'ITEM_PRICING_TABLE.COLUMNS.QUANTITY' | translate }}\n \n \n
\n @if (config?.quantityEditable && item.isMultiactivatable && item.type === 'addon') {\n \n \n \n } @else {\n {{ item.quantity }}\n }\n
\n
\n
\n\n \n \n {{ 'ITEM_PRICING_TABLE.COLUMNS.WHOLESALE' | translate }}\n \n \n
\n
\n \n \n \n \n
\n
\n \n 0; else noFee\"\n [align]=\"'end'\"\n [price]=\"fee.wholesaleAmount\"\n [currencyCode]=\"item.wholesalePricing.currency\"\n [frequency]=\"fee.type === 'setup' ? Frequency.ONE_TIME : item.wholesalePricing.frequency\"\n />\n \n
\n
\n
\n
\n\n \n \n {{ 'ITEM_PRICING_TABLE.COLUMNS.RETAIL' | translate }}\n \n \n
\n
\n \n
\n
\n \n = 0; else noFee\"\n [value]=\"fee.retailAmount | centsToDollars\"\n [currencyCode]=\"item.retailPricing.currency\"\n [minimumValue]=\"'0'\"\n [maxWidth]=\"220\"\n (change)=\"onPriceChange($event, item, 'setupfee')\"\n [bottomSpacing]=\"'none'\"\n data-cy=\"retail-fee\"\n />\n \n
\n
\n
\n
\n\n \n \n {{ 'ITEM_PRICING_TABLE.COLUMNS.FREQUENCY' | translate }}\n \n \n
\n \n
\n
\n
\n\n \n \n {{ 'ITEM_PRICING_TABLE.COLUMNS.RETAIL_TOTAL' | translate }}\n \n \n
\n
\n \n
\n
\n \n 0; else noFee\"\n [align]=\"'end'\"\n [price]=\"fee.retailAmountTotal\"\n [currencyCode]=\"item.retailPricing.currency\"\n [frequency]=\"Frequency.ONE_TIME\"\n />\n \n
\n
\n
\n
\n\n \n \n \n
\n
\n \n \n \n {{ 'ITEM_PRICING_TABLE.ACTION_MENU.EDIT_BILLING_TERMS' | translate }}\n \n \n {{ 'ITEM_PRICING_TABLE.ACTION_MENU.ADD_BILLING_TERMS' | translate }}\n \n \n {{ 'ITEM_PRICING_TABLE.ACTION_MENU.ADD_SETUP_FEE' | translate }}\n \n \n @if (config?.selectionEditable) {\n \n }\n \n
\n\n \n
\n \n
\n
\n
\n
\n
\n \n
\n\n @if (!config?.hidePriceSummary) {\n \n \n
\n
\n {{ 'ITEM_PRICING_TABLE.FOOTER.TOTALS' | translate }}\n
\n\n
\n
\n
\n {{ 'ITEM_PRICING_TABLE.FOOTER.WHOLESALE' | translate }}\n
\n
\n \n
\n
\n\n
\n
\n {{ 'ITEM_PRICING_TABLE.FOOTER.RETAIL' | translate }}\n
\n
\n \n
\n
\n
\n
\n
\n
\n }\n\n
\n
\n\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialogModule } from '@angular/material/dialog';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { MatTableModule } from '@angular/material/table';\nimport { BillingUiModule } from '@vendasta/billing-ui';\nimport { GalaxyAvatarModule } from '@vendasta/galaxy/avatar';\nimport { GalaxyFormFieldModule } from '@vendasta/galaxy/form-field';\nimport { GalaxyFrequencyModule } from '@vendasta/galaxy/frequency';\nimport { GalaxyInputModule } from '@vendasta/galaxy/input';\nimport { GalaxyTableModule } from '@vendasta/galaxy/table';\nimport { ItemCommonModule } from '../common/item-common.module';\nimport { PackageContentsPopoverModule } from '../package-contents-popover/package-contents-popover.module';\nimport { ItemPricingTableComponent } from './item-pricing-table.component';\nimport { MultiActivatableDialogComponent } from './multi-activatable-dialog/multi-activatable-dialog.component';\nimport { FormatWholesaleMinimumHintPipe } from './pipes/format-wholesale-minimum-hint.pipe';\nimport { HasSetupFeePipe } from './pipes/has-setup-fee.pipe';\nimport { IsSetupFeeRemovablePipe } from './pipes/is-setup-fee-removable.pipe';\nimport { GalaxyFrequencyToRevenuePeriodPipe } from '../billing-term-dialog/sales-to-galaxy-frequency.pipe';\nimport { FormatBillingTermPipe } from './pipes/format-billing-term.pipe';\n\nexport const ITEM_PRICING_TABLE_DECLARATIONS = [\n ItemPricingTableComponent,\n MultiActivatableDialogComponent,\n FormatWholesaleMinimumHintPipe,\n HasSetupFeePipe,\n IsSetupFeeRemovablePipe,\n FormatBillingTermPipe,\n];\n\nexport const ITEM_PRICING_TABLE_IMPORTS = [\n CommonModule,\n ItemCommonModule,\n MatButtonModule,\n MatDialogModule,\n MatIconModule,\n MatMenuModule,\n MatTableModule,\n BillingUiModule,\n GalaxyFormFieldModule,\n GalaxyAvatarModule,\n GalaxyFrequencyModule,\n GalaxyInputModule,\n GalaxyTableModule,\n PackageContentsPopoverModule,\n GalaxyFrequencyToRevenuePeriodPipe,\n];\n\n@NgModule({\n exports: [ItemPricingTableComponent],\n declarations: ITEM_PRICING_TABLE_DECLARATIONS,\n imports: ITEM_PRICING_TABLE_IMPORTS,\n})\nexport class ItemPricingTableModule {}\n", "{\n \"COMMON\": {\n \"ACTIONS\": {\n \"ADD_ITEMS\": \"Add items\",\n \"ADD_TO_ORDER\": \"Add to order\",\n \"CANCEL\": \"Cancel\",\n \"CLEAR\": \"Clear\",\n \"REMOVE\": \"Remove\",\n \"SELECT\": \"Select\",\n \"SAVE\": \"Save\"\n },\n \"ITEM_TYPE\": {\n \"PRODUCT\": \"Product\",\n \"PRODUCTS\": \"Products\",\n \"SERVICE\": \"Service\",\n \"SERVICES\": \"Services\",\n \"PACKAGE\": \"Package\",\n \"PACKAGES\": \"Packages\",\n \"ALL_ITEMS\": \"All items\"\n },\n \"UNAVAILABLE_ITEM\": {\n \"SUSPENDED\": {\n \"SUBTEXT\": \"Unavailable\",\n \"TOOLTIP\": \"This item has been suspended, and is no longer available for new orders\"\n },\n \"COUNTRY_RESTRICTION\": {\n \"SUBTEXT\": \"Unavailable in {{country}}\",\n \"TOOLTIP\": \"Unavailable to customers located in {{country}}\",\n \"COUNTRY_FALLBACK\": \"your country\"\n },\n \"ALREADY_ACTIVE\": {\n \"SUBTEXT\": \"Already active\",\n \"TOOLTIP\": \"This item is already active\"\n },\n \"DEFAULT\": {\n \"SUBTEXT\": \"Unavailable\",\n \"TOOLTIP\": \"This item is not available\"\n }\n }\n },\n \"ACTIVE_ITEMS\": {\n \"TITLE\": \"Currently active products\",\n \"STATUS\": {\n \"ACTIVE\": \"Active\",\n \"CANCELED\": \"Canceled\",\n \"PENDING\": \"Pending\",\n \"DEMO\": \"Demo\",\n \"DEMO_EXPIRED\": \"Demo expired\"\n }\n },\n \"ITEM_SELECTOR\": {\n \"SHOW_UNAVAILABLE\": \"Show unavailable\",\n \"SHOW_ADDONS\": \"Show add-ons\",\n \"COLUMN\": {\n \"NAME\": \"Name\",\n \"WHOLESALE\": \"Wholesale\",\n \"RETAIL\": \"Retail\"\n },\n \"REQUIRED_ITEMS_LABEL\": \"Required items to be active\",\n \"ITEM_TYPE_BY_OWNER\": \"{{itemType}} by {{owner}}\",\n \"CONFLICT_RESOLUTION_MODAL\": {\n \"TITLE\": \"Add to order?\",\n \"MESSAGE\": \"One or more products in this package are already selected or active. Add the remaining products to this order?\"\n },\n \"DEPENDENCIES\": {\n \"SELECTION_NOTIFICATION\": \"{{itemNames}} selected as well\",\n \"REMOVAL_NOTIFICATION\": \"{{itemNames}} removed as well\",\n \"REMOVAL_DUE_TO_EDITION_CHANGE\": \"{{affectedItemNames}} removed because required edition of {{triggerItemName}} changed\",\n \"REQUIRED_ITEMS_UNAVAILABLE\": \"Missing required items \u2014 Contact an administrator to ensure that all requirements are available for this item\"\n },\n \"VIEW_ADDONS\": \"View add-ons\"\n },\n \"ITEM_SEARCH_SELECT_FILTER\": {\n \"LABEL\": \"Select item\",\n \"ADDON_LABEL\": \"Select add-on\",\n \"PRODUCT_LABEL\": \"Select product\",\n \"SEARCH\": \"Search\"\n },\n \"ITEM_SELECTION_CART\": {\n \"TITLE\": \"Selected items\",\n \"NO_ITEMS_SELECTED\": \"No item selected\",\n \"CLEAR_ALL_CONFIRMATION_MODAL\": {\n \"TITLE\": \"Clear selected items?\",\n \"MESSAGE\": \"This action cannot be undone.\"\n },\n \"REMOVE_ITEM_CONFIRMATION_MODAL\": {\n \"TITLE\": \"Remove item?\",\n \"MESSAGE\": \"This will also remove the {{parentItemName}} package from this order. The rest of its contents will stay on this order, but this may change the total retail price of the order.\"\n }\n },\n \"ITEM_PRICING_TABLE\": {\n \"COLUMNS\": {\n \"ITEM\": \"Item\",\n \"QUANTITY\": \"Quantity\",\n \"WHOLESALE\": \"Wholesale\",\n \"RETAIL\": \"Retail\",\n \"FREQUENCY\": \"Frequency\",\n \"RETAIL_TOTAL\": \"Retail total\"\n },\n \"ACTION_MENU\": {\n \"ADD_SETUP_FEE\": \"Add setup fee\",\n \"RESET_TO_DEFAULT\": \"Reset to default\",\n \"REMOVE_ITEM\": \"Remove item\",\n \"EDIT_BILLING_TERMS\": \"Edit billing terms\",\n \"ADD_BILLING_TERMS\": \"Add billing terms\"\n },\n \"FOOTER\": {\n \"TOTALS\": \"Totals\",\n \"WHOLESALE\": \"Wholesale\",\n \"RETAIL\": \"Retail\"\n }\n },\n \"BILLING_FREQUENCY\": {\n \"ONE_TIME\": \"One time\",\n \"MONTHLY\": \"Monthly\",\n \"YEARLY\": \"Yearly\"\n },\n \"EDIT_BILLING_TERMS_DIALOG\": {\n \"TITLE\": \"Edit billing terms\",\n \"FORM\": {\n \"START_DATE\": \"Start date\",\n \"ESTIMATED_END_DATE\": \"Estimated end date: {{estimatedEndDate}}\",\n \"NOT_APPLICABLE\": \"N/A\",\n \"TERM_LENGTH\": \"Term length\",\n \"BILLING_TERMS\": \"Billing terms\",\n \"FIXED_TERM\": \"Fixed number of payments\",\n \"INDEFINITE_TERM\": \"Automatically renew until cancelled\",\n \"BILLING_FREQUENCY_ONE_TIME\": \"one-time\",\n \"BILLING_FREQUENCY_MONTH\": \"month\",\n \"BILLING_FREQUENCY_MONTHS\": \"months\",\n \"BILLING_FREQUENCY_YEAR\": \"year\",\n \"BILLING_FREQUENCY_YEARS\": \"years\"\n }\n },\n \"BILLING_PERIOD\": {\n \"START_DATE_ONE_MONTH\": \"{{startDate}} (1 month)\",\n \"START_DATE_MANY_MONTHS\": \"{{startDate}} ({{numPeriods}} months)\",\n \"START_DATE_ONE_YEAR\": \"{{startDate}} (1 year)\",\n \"START_DATE_MANY_YEARS\": \"{{startDate}} ({{numPeriods}} years)\",\n \"SAVE_AND_ADD_ANOTHER_TERM\": \"Save and add another term\"\n }\n}\n", "import { NgModule } from '@angular/core';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport { TranslateModule } from '@ngx-translate/core';\nimport En from './assets/en_devel.json';\n\n@NgModule({\n imports: [\n TranslateModule,\n LexiconModule.forChild({\n componentName: 'common/inventory',\n baseTranslation: En,\n }),\n ],\n})\nexport class InventoryUiI18nModule {}\n", "import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { map, shareReplay } from 'rxjs/operators';\nimport { InventoryItem } from '../common/inventory-item';\nimport { SelectedItem } from '../item-selector/interface';\nimport { PackageContentsService } from './package-contents.service';\n\n@Injectable()\nexport class ItemConflictService {\n constructor(private packageContentsService: PackageContentsService) {}\n\n resolveItemConflicts(\n selectedItem: SelectedItem,\n availableItems: SelectedItem[],\n partnerId: string,\n marketId: string,\n ): Observable {\n const duplicateItem = availableItems.find((i) => i.itemId === selectedItem.itemId);\n if (duplicateItem) {\n return of([]);\n }\n\n const packageItemsMap$ = this.getPackagedItemsMap(selectedItem, availableItems, partnerId, marketId);\n return packageItemsMap$.pipe(\n map((packageItemsMap) => {\n const itemsToCheck = packageItemsMap.get(selectedItem.itemId) ?? [selectedItem];\n packageItemsMap.delete(selectedItem.itemId);\n return this.getItemsToSelect(selectedItem, itemsToCheck, packageItemsMap, availableItems);\n }),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n private getPackagedItemsMap(\n selectedItem: SelectedItem,\n availableItems: SelectedItem[],\n partnerId: string,\n marketId: string,\n ): Observable> {\n const availablePackageItems = availableItems.filter((i) => i.type === 'package');\n const allPackageIds = availablePackageItems.map((i) => i.itemId);\n if (selectedItem.type === 'package') {\n allPackageIds.push(selectedItem.itemId);\n }\n return this.packageContentsService.fetchMultiPackageItems(allPackageIds, partnerId, marketId);\n }\n\n private getItemsToSelect(\n selectedItem: SelectedItem,\n itemsToCheck: SelectedItem[],\n packageItemsMap: Map,\n availableItems: SelectedItem[],\n ): SelectedItem[] {\n const allProductItems = availableItems.filter((i) => i.type !== 'package');\n const allPackagedItems = Array.from(packageItemsMap.values()).flat();\n const allAvailableItems = allProductItems.concat(allPackagedItems);\n const uniqueItems = itemsToCheck.filter((i) => !allAvailableItems.some((ai) => ai.itemId === i.itemId));\n if (uniqueItems.length === itemsToCheck.length) {\n return [selectedItem];\n }\n return uniqueItems;\n }\n}\n", "import { Injectable } from '@angular/core';\nimport {\n App,\n AppKey,\n GetMultiAppRequest,\n ItemIdentifier,\n ItemsApiService,\n ListDependenciesRequest,\n ListDependenciesRequiredItem,\n ListDependenciesRequiredItems,\n PartnerApiService,\n} from '@galaxy/marketplace-apps';\nimport { Observable, of } from 'rxjs';\nimport { map, shareReplay, switchMap } from 'rxjs/operators';\nimport { InventoryItem, InventoryItemApp } from '../common/inventory-item';\nimport { areEditionIdsEqual, mapify } from '../item-selector/util';\n\nexport type InventoryDependencyItem = InventoryItem & {\n enabled?: boolean;\n};\n\ntype MissingItems = { [key: string]: ListDependenciesRequiredItems };\n\n@Injectable()\nexport class ItemDependencyService {\n constructor(\n private itemsApiService: ItemsApiService,\n private partnerApiService: PartnerApiService,\n ) {}\n\n listMissingRequirementsForItems(\n partnerId: string,\n marketId: string,\n targetItems: InventoryItem[],\n availableItems: InventoryItem[],\n ): Observable> {\n // We are only concerned about app and add-on dependencies\n const filteredTargetItems = targetItems.filter((ti) => ti.type !== 'package');\n if (!filteredTargetItems.length) {\n return of(new Map());\n }\n\n // We only need to be concerned with available apps because only apps can satisfy dependencies\n const filteredAvailableItems = availableItems.filter((ai) => ai.type === 'app');\n return this.listMissingItems(partnerId, filteredTargetItems).pipe(\n switchMap((missingItems) => {\n const missingItemsMap = this.mapifyMissingItems(missingItems);\n const filteredMissingItemsMap = this.filterMissingItems(missingItemsMap, filteredAvailableItems);\n return this.convertMissingItemsToInventoryDependencyItems(partnerId, marketId, filteredMissingItemsMap);\n }),\n );\n }\n\n private listMissingItems(partnerId: string, targetItems: InventoryItem[]): Observable {\n const itemIdentifiers = targetItems.map((ti) => {\n const itemId = ti.itemId;\n let editionId = '';\n if (ti.type === 'app') {\n const app = ti as InventoryItemApp;\n editionId = app.edition?.editionId ?? '';\n }\n return new ItemIdentifier({ itemId: itemId, editionId: editionId });\n });\n return this.itemsApiService\n .listDependencies(\n new ListDependenciesRequest({\n sellerId: partnerId,\n items: itemIdentifiers,\n }),\n )\n .pipe(\n map((resp) => resp?.missingItems ?? {}),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n private mapifyMissingItems(missingItems: MissingItems): Map {\n return mapify(\n Object.entries(missingItems) || [],\n ([itemId]) => itemId,\n ([, requiredItems]) => requiredItems.requiredItems,\n );\n }\n\n private filterMissingItems(\n missingItemsMap: Map,\n availableItems: InventoryItem[],\n ): Map {\n const availableApps = availableItems as InventoryItemApp[];\n const filteredMissingItems = new Map();\n missingItemsMap.forEach((rawRequiredItems, itemId) => {\n const filteredRequiredItems =\n rawRequiredItems?.filter((ri) => {\n const item = availableApps.find(\n (i) =>\n i.itemId === ri.itemId &&\n (!ri.editionRequirement?.editionRequired ||\n areEditionIdsEqual(i.edition?.editionId, ri.editionRequirement?.editionId)),\n );\n return !item;\n }) ?? [];\n if (filteredRequiredItems?.length) {\n filteredMissingItems.set(itemId, filteredRequiredItems);\n }\n });\n return filteredMissingItems;\n }\n\n private convertMissingItemsToInventoryDependencyItems(\n partnerId: string,\n marketId: string,\n missingItemsMap: Map,\n ): Observable> {\n const requiredItems = Array.from(missingItemsMap.values()).flat();\n const requiredAppKeys = requiredItems.map((ri) => new AppKey({ appId: ri.itemId }));\n const appsMap$ = this.getAppsMap(partnerId, marketId, requiredAppKeys);\n return appsMap$.pipe(\n map((appsMap) => {\n const inventoryItemsMap = new Map();\n missingItemsMap.forEach((requiredItems, itemId) => {\n const inventoryItems = requiredItems.map((ri) => {\n const app = appsMap.get(ri.itemId);\n return this.requiredItemToInventoryDependencyItem(ri, app!);\n });\n inventoryItemsMap.set(itemId, inventoryItems);\n });\n return inventoryItemsMap;\n }),\n );\n }\n\n private getAppsMap(partnerId: string, marketId: string, appKeys: AppKey[]): Observable> {\n if (!appKeys.length) {\n return of(new Map());\n }\n\n return this.partnerApiService\n .getMultiApp(\n new GetMultiAppRequest({\n partnerId: partnerId,\n marketId: marketId,\n appKeys: appKeys,\n projectionFilter: { paths: ['key', 'sharedMarketingInformation', 'editionInformation'] },\n }),\n )\n .pipe(\n map((resp) =>\n mapify(\n resp.apps ?? [],\n (app) => app.key?.appId,\n (app) => app,\n ),\n ),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n private requiredItemToInventoryDependencyItem(\n requiredItem: ListDependenciesRequiredItem,\n app: App,\n ): InventoryDependencyItem {\n const editions = app?.editionInformation?.editions || [];\n let edition = null;\n if (editions.length > 0) {\n // If a specific edition is required, find it. Otherwise, default to the first edition.\n if (requiredItem.editionRequirement?.editionRequired) {\n edition = editions.find((e) =>\n areEditionIdsEqual(e.appKey?.editionId, requiredItem.editionRequirement?.editionId),\n );\n } else {\n edition = editions[0];\n }\n }\n return {\n itemId: requiredItem.itemId,\n edition: {\n editionId: edition?.appKey?.editionId ?? '',\n name: edition?.name ?? '',\n },\n name: app?.sharedMarketingInformation?.name ?? '',\n iconUrl: app?.sharedMarketingInformation?.iconUrl ?? '',\n type: 'app',\n quantity: 1,\n enabled: requiredItem.enabled ?? false,\n };\n }\n}\n", "import { Injectable } from '@angular/core';\n\nexport interface LineItem {\n itemId: string;\n editionId?: string;\n itemType: 'app' | 'addon' | 'package';\n quantity: number;\n}\n\nconst LINE_ITEMS_CACHE_KEY = 'inventory/line-items-cache';\n\n@Injectable()\nexport class LineItemCacheService {\n private buildKey(businessId: string): string {\n return `${LINE_ITEMS_CACHE_KEY}:${businessId}`;\n }\n\n readLineItems(businessId: string): LineItem[] {\n const key = this.buildKey(businessId);\n const lineItems = sessionStorage.getItem(key);\n if (!lineItems?.length) return [];\n\n return JSON.parse(lineItems);\n }\n\n cacheLineItems(businessId: string, lineItems: LineItem[]): void {\n const key = this.buildKey(businessId);\n if (!lineItems?.length) {\n sessionStorage.removeItem(key);\n return;\n }\n\n sessionStorage.setItem(key, JSON.stringify(lineItems));\n }\n\n clearLineItemCache(businessId: string): void {\n const key = this.buildKey(businessId);\n sessionStorage.removeItem(key);\n }\n}\n", "import { Injectable, Signal, WritableSignal, computed, signal } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { map, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators';\nimport { InventoryItemApp } from '../common/inventory-item';\nimport { ItemConflictService } from '../services/item-conflict.service';\nimport { InventoryDependencyItem, ItemDependencyService } from '../services/item-dependency.service';\nimport { SelectedItem } from './interface';\nimport { convertInventoryItemToSelectedItem, findItem } from './util/item';\n\nexport interface SelectionOptions {\n partnerId: string;\n marketId: string;\n confirmItenComflictResolutionCallback?: () => Observable;\n enforceDependencies?: boolean;\n}\n\ntype SelectionEventType = 'selected' | 'deselected';\n\ntype SelectionEventSource =\n | 'user'\n | 'conflict-duplicate-item'\n | 'conflict-intersecting-items'\n | 'dependency-selection-change'\n | 'dependency-edition-change'\n | 'dependency-unavailable';\n\nexport interface SelectionEvent {\n triggerItem?: SelectedItem;\n updatedItems: SelectedItem[];\n eventType: SelectionEventType;\n eventSource: SelectionEventSource;\n}\n\n@Injectable()\nexport class ItemSelectorServiceV2 {\n private _selectedItems: WritableSignal = signal([]);\n private _immutableSelectedItems: WritableSignal = signal([]);\n private _changeEvents: WritableSignal = signal([]);\n\n public readonly selectedItems: Signal = this._selectedItems.asReadonly();\n public readonly immutableSelectedItems: Signal = this._immutableSelectedItems.asReadonly();\n public readonly changeEvents: Signal = this._changeEvents.asReadonly();\n\n public readonly allItems: Signal = computed(() => {\n const selectedItems = this._selectedItems();\n const immutableSelectedItems = this._immutableSelectedItems();\n return [...selectedItems, ...immutableSelectedItems];\n });\n\n constructor(\n private itemConflictService: ItemConflictService,\n private itemDependencyService: ItemDependencyService,\n ) {}\n\n public initSelectedItems(value: SelectedItem[]) {\n this._selectedItems.set(value ?? []);\n // For now, reset the change events when the items are selected to deal\n // with the circular dependency between the item selector and the activation\n // service causing items to be re-selected from a different \"selection session\"\n // for a different account.\n this._changeEvents.set([]);\n }\n\n public initImmutableSelectedItems(items: SelectedItem[]) {\n const immutableItems = items?.map((i) => ({ ...i, immutable: true }));\n this._immutableSelectedItems.set(immutableItems ?? []);\n }\n\n public updateSelection(isSelected: boolean, targetItem: SelectedItem, options?: SelectionOptions): void {\n if (isSelected) {\n this.addSelection(targetItem, 'user', options);\n return;\n }\n\n const [, index] = findItem(this.selectedItems(), targetItem.itemId);\n if (isIndexValid(index)) {\n this.removeSelection(index, 'user', options);\n }\n }\n\n public updateItemEdition(targetItem: SelectedItem, options?: SelectionOptions): void {\n const [, index] = findItem(this.selectedItems(), targetItem.itemId);\n\n if (!isIndexValid(index)) {\n if (targetItem.immutable) {\n this.updateSelection(\n true,\n {\n ...targetItem,\n quantity: targetItem?.quantity || 1,\n },\n options,\n );\n }\n return;\n }\n\n this._selectedItems.update((selectedItems) => {\n const selectedItem = selectedItems[index];\n if (selectedItem.type === 'app') {\n const targetApp = targetItem as InventoryItemApp;\n selectedItem.edition = targetApp.edition;\n }\n return [...selectedItems];\n });\n const updatedItem = this.selectedItems()[index];\n this.pushSelectionEvent([updatedItem], 'selected', 'user');\n if (updatedItem.type === 'package' || !options?.enforceDependencies) {\n return;\n }\n\n const selectedItems = this.selectedItems();\n this.removeBrokenDependencies(\n options.partnerId,\n options.marketId,\n selectedItems,\n 'dependency-edition-change',\n updatedItem,\n );\n }\n\n public updateItemQuantity(targetItem: SelectedItem, options?: SelectionOptions): void {\n const [, index] = findItem(this.selectedItems(), targetItem.itemId);\n const quantity = targetItem.quantity;\n\n // add item with quantity\n if (!isIndexValid(index)) {\n this.addSelection(targetItem, 'user', options);\n return;\n }\n\n // update item quantity\n if (quantity > 0) {\n this._selectedItems.update((selectedItems) => {\n selectedItems[index].quantity = quantity;\n return [...selectedItems];\n });\n const updatedItem = this.selectedItems()[index];\n this.pushSelectionEvent([updatedItem], 'selected', 'user');\n return;\n }\n\n // remove selected item\n if (quantity <= 0) {\n this.removeSelection(index, 'user', options);\n }\n }\n\n public removeItemId(itemId: string, eventSource: SelectionEventSource, options?: SelectionOptions): void {\n const [, index] = findItem(this.selectedItems(), itemId);\n if (isIndexValid(index)) {\n this.removeSelection(index, eventSource, options);\n }\n }\n\n public clearSelection(clearedByUser = false): void {\n if (clearedByUser) {\n this.pushSelectionEvent([...this.selectedItems()], 'deselected', 'user');\n }\n this._selectedItems.set([]);\n }\n\n private addSelection(\n selectedItem: SelectedItem,\n eventSource: SelectionEventSource,\n options?: SelectionOptions,\n ): void {\n const selectedItems = this.selectedItems();\n const activeItems = this.immutableSelectedItems();\n this._selectedItems.update((selectedItems) => [...selectedItems, selectedItem]);\n this.pushSelectionEvent([selectedItem], 'selected', eventSource);\n this.validateItemSelections(selectedItem, selectedItems, activeItems, options);\n }\n\n private removeSelection(itemIndex: number, eventSource: SelectionEventSource, options?: SelectionOptions): void {\n const selectedItems = this._selectedItems();\n const [removedItem] = selectedItems.splice(itemIndex, 1);\n this._selectedItems.set([...selectedItems]);\n this.pushSelectionEvent([removedItem], 'deselected', eventSource);\n\n if (removedItem.type === 'package' || !options?.enforceDependencies) {\n return;\n }\n\n this.removeBrokenDependencies(\n options.partnerId,\n options.marketId,\n selectedItems,\n 'dependency-selection-change',\n removedItem,\n );\n }\n\n private validateItemSelections(\n selectedItem: SelectedItem,\n selectedItems: SelectedItem[],\n activeItems: SelectedItem[],\n options?: SelectionOptions,\n ): void {\n // When resolving item conflicts, only consider the selected items so multi-activatable items\n // can be selected again, even if there are active.\n const availableItems = [...selectedItems, ...activeItems];\n const resolvedItems$ = this.resolveItemConflicts(selectedItem, selectedItems, options);\n const missingItems$ = resolvedItems$.pipe(\n switchMap((resolvedItems) => {\n const updatedItems = this.replaceSelectedItemWithResolvedItems(selectedItem, selectedItems, resolvedItems);\n return this.findMissingDependencies(updatedItems, availableItems, options);\n }),\n );\n // TODO - refactor this when the item selector service supports selecting multiple items simultaneously\n // because not doing this causes a flicker when a package is decomposed into its contents\n resolvedItems$.pipe(take(1)).subscribe((resolvedItems) => this.handleItemConflicts(selectedItem, resolvedItems));\n if (!options?.enforceDependencies) {\n return;\n }\n missingItems$\n .pipe(take(1))\n .subscribe((missingItems) => this.handleMissingDependencies(selectedItem, missingItems, options));\n }\n\n private resolveItemConflicts(\n selectedItem: SelectedItem,\n availableItems: SelectedItem[],\n options?: SelectionOptions,\n ): Observable {\n const resolvedItems$ = this.itemConflictService\n .resolveItemConflicts(selectedItem, availableItems, options?.partnerId ?? '', options?.marketId ?? '')\n .pipe(\n map((resolvedItems) => resolvedItems.map((ri) => convertInventoryItemToSelectedItem(ri, false))),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n return resolvedItems$.pipe(\n switchMap((resolvedItems) => {\n const shouldConfirm = resolvedItems?.length && resolvedItems[0].itemId !== selectedItem.itemId;\n if (!shouldConfirm || !options?.confirmItenComflictResolutionCallback) {\n return of(true);\n }\n return options.confirmItenComflictResolutionCallback();\n }),\n withLatestFrom(resolvedItems$),\n map(([confirmation, resolvedItems]) => {\n if (!confirmation) {\n return [];\n }\n return resolvedItems ?? [];\n }),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n private handleItemConflicts(selectedItem: SelectedItem, resolvedItems: SelectedItem[]): void {\n if (resolvedItems?.length === 1 && resolvedItems[0].itemId === selectedItem.itemId) {\n // In this case, there are no conflicts and the item is already selected.\n return;\n }\n\n this._selectedItems.update((selectedItems) => [\n ...this.replaceSelectedItemWithResolvedItems(selectedItem, selectedItems, resolvedItems),\n ]);\n\n let updatedItems = [selectedItem];\n let eventType: SelectionEventType = 'deselected';\n let eventSource: SelectionEventSource = 'conflict-duplicate-item';\n if (resolvedItems?.length) {\n updatedItems = resolvedItems;\n eventType = 'selected';\n eventSource = 'conflict-intersecting-items';\n }\n this.pushSelectionEvent(updatedItems, eventType, eventSource);\n }\n\n private replaceSelectedItemWithResolvedItems(\n selectedItem: SelectedItem,\n selectedItems: SelectedItem[],\n resolvedItems: SelectedItem[],\n ): SelectedItem[] {\n const filteredItems = selectedItems.filter((si) => si.itemId !== selectedItem.itemId);\n return [...filteredItems, ...resolvedItems];\n }\n\n private findMissingDependencies(\n targetItems: SelectedItem[],\n availableItems: SelectedItem[],\n options?: SelectionOptions,\n ): Observable> {\n return this.itemDependencyService\n .listMissingRequirementsForItems(options?.partnerId ?? '', options?.marketId ?? '', targetItems, availableItems)\n .pipe(shareReplay({ bufferSize: 1, refCount: true }));\n }\n\n private handleMissingDependencies(\n triggerItem: SelectedItem,\n missingItemsMap: Map,\n options?: SelectionOptions,\n ): void {\n missingItemsMap.forEach((missingItems, targetItemId) => {\n if (missingItems.some((mi) => !mi.enabled)) {\n this.removeItemId(targetItemId, 'dependency-unavailable', {\n partnerId: options?.partnerId ?? '',\n marketId: options?.marketId ?? '',\n enforceDependencies: false,\n });\n return;\n }\n\n const requiredItems = missingItems.map((mi) => convertInventoryItemToSelectedItem(mi, false));\n this._selectedItems.update((selectedItems) => {\n requiredItems.forEach((requiredItem) => {\n const [targetItem, targetItemIdx] = findItem(selectedItems, requiredItem.itemId);\n if (!isIndexValid(targetItemIdx)) {\n selectedItems.push(requiredItem);\n } else {\n if (targetItem?.type === 'app' && requiredItem?.type === 'app') {\n const targetApp = targetItem as InventoryItemApp;\n const requiredApp = requiredItem as InventoryItemApp;\n targetApp.edition = requiredApp.edition;\n }\n }\n });\n return [...selectedItems];\n });\n this.pushSelectionEvent(requiredItems, 'selected', 'dependency-selection-change', triggerItem);\n });\n }\n\n private removeBrokenDependencies(\n partnerId: string,\n marketId: string,\n targetItems: SelectedItem[],\n eventSource: SelectionEventSource,\n triggerItem?: SelectedItem,\n ): void {\n const selectedItems = this.selectedItems();\n // TODO - replace this with active items?\n const activeItems = this.immutableSelectedItems();\n const availabeItems = selectedItems.concat(activeItems);\n this.itemDependencyService\n .listMissingRequirementsForItems(partnerId, marketId, targetItems, availabeItems)\n .pipe(\n take(1),\n map((missingItemsMap) => Array.from(missingItemsMap.keys())),\n )\n .subscribe((brokenItemIds) => {\n if (!brokenItemIds?.length) {\n return;\n }\n\n const selectedItems = this._selectedItems();\n const removedItems: SelectedItem[] = [];\n brokenItemIds.forEach((brokenItemId) => {\n const [, brokenItemIdx] = findItem(selectedItems, brokenItemId);\n const [removedItem] = selectedItems.splice(brokenItemIdx, 1);\n removedItems.push(removedItem);\n });\n this._selectedItems.set([...selectedItems]);\n this.pushSelectionEvent(removedItems, 'deselected', eventSource, triggerItem);\n });\n }\n\n private pushSelectionEvent(\n updatedItems: SelectedItem[],\n eventType: SelectionEventType,\n eventSource: SelectionEventSource,\n triggerItem?: SelectedItem,\n ) {\n this._changeEvents.update((changeEvents) => {\n return [{ triggerItem, updatedItems, eventType, eventSource }, ...changeEvents];\n });\n }\n}\n\nfunction isIndexValid(index: number): boolean {\n return index >= 0;\n}\n", "import {\n ActivatableItemSummaryInterface,\n AppPrices,\n ChildApp,\n ItemType,\n ListActivatableItemsFilters,\n ListActivatableItemsRequest,\n} from '@vendasta/marketplace-apps';\nimport { InventoryItem } from '../common/inventory-item';\nimport { InventoryItemEdition } from '../common/inventory-item/item-types/app';\n\nexport interface ItemSelectorRowItem extends ActivatableItemSummaryInterface {\n itemId: string;\n appPrices?: AppPrices;\n isSelected?: boolean;\n editionId?: string;\n quantity?: number;\n editions?: Edition[];\n itemType?: ItemType;\n immutable?: boolean;\n unavailableInCountry?: boolean;\n pricesInitialized?: boolean;\n childApps?: ChildApp[];\n}\n\nexport interface Edition extends InventoryItemEdition {\n appId: string;\n isActive?: boolean;\n}\n\nexport type SelectedItem = InventoryItem & {\n immutable?: boolean;\n};\n\nexport interface ItemSelectorConfig {\n partnerId?: string;\n marketId?: string;\n businessId?: string;\n pageSize?: number;\n country?: string;\n priceType?: 'generic' | 'instance';\n hideOwnerName?: boolean;\n hideWholesale?: boolean;\n hideRetail?: boolean;\n hideQuantity?: boolean;\n hideItemsWithOrderForms?: boolean;\n itemTypesAvailable?: ItemSelectorFilterType[];\n enforceDependencies?: boolean;\n}\n\nexport type ItemSelectorFilterType = 'products' | 'packages';\n\nexport enum SelectionChangeEventType {\n SELECTED,\n DESELECTED,\n}\n\nexport interface SelectionChange {\n selectedItems: SelectedItem[];\n updatedItem: SelectedItem;\n eventType: SelectionChangeEventType;\n}\n\nexport type ItemSelectorFilters = Omit<\n ListActivatableItemsFilters,\n 'searchTerm' | 'itemType' | 'ownerPartnerId' | 'toApiJson'\n>;\n\nexport interface ItemSelectorState {\n itemType: 'products' | 'packages' | 'allItems';\n requiredItem: ItemSelectorRowItem | null;\n showAddons: boolean;\n showUnavailableItems: boolean;\n}\n\nexport type FilterFn = (req: ListActivatableItemsRequest) => ListActivatableItemsRequest;\n", "import { GalaxyColumnDef } from '@vendasta/galaxy/table/src/table.interface';\n\nexport const ITEMS_SELECTOR_COLUMNS: GalaxyColumnDef[] = [\n {\n id: 'checkbox',\n title: 'Checkbox',\n },\n {\n id: 'name',\n title: 'Name',\n },\n {\n id: 'quantityInput',\n title: 'Quantity',\n },\n {\n id: 'wholesale',\n title: 'Wholesale',\n },\n {\n id: 'retail',\n title: 'Retail',\n },\n];\n", "import { PagedListRequestInterface, PagedResponseInterface, PaginatedAPIInterface } from '@vendasta/galaxy/table';\nimport {\n App,\n AppPrice,\n AppPrices,\n AppPricingApiService,\n FieldMask,\n GetMultiPricingRequest,\n GetMultiPricingResponse,\n ItemsApiService,\n ListActivatableItemsRequest,\n ListActivatableItemsSortOptions,\n PartnerApiService,\n RequestedApp,\n} from '@vendasta/marketplace-apps';\nimport {\n EditionsApiService,\n ListByMultiAppRequest,\n ListByMultiAppResponseAppEditions,\n} from '@vendasta/marketplace-apps/v1';\nimport { Observable, combineLatest, merge, of } from 'rxjs';\nimport { catchError, map, shareReplay, skipWhile, startWith, switchMap } from 'rxjs/operators';\nimport { ItemSelectorConfig, ItemSelectorFilters, ItemSelectorRowItem, SelectedItem } from './interface';\nimport { ItemSelectorServiceV2 } from './item-selector-v2.service';\nimport { convertToItemSelectorEdition, findItem, findItemEdition, markActiveEdition } from './util';\n\nexport class ItemSelectorDataSource implements PaginatedAPIInterface {\n constructor(\n private readonly itemsApiService: ItemsApiService,\n private readonly appPricingApiService: AppPricingApiService,\n private readonly editionsService: EditionsApiService,\n private readonly sortOptions$: Observable,\n private readonly itemSelectorServiceV2: ItemSelectorServiceV2,\n private readonly immutableSelectedItems$: Observable,\n private readonly selectedItems$: Observable,\n private readonly config: Required,\n private readonly partnerApiService: PartnerApiService,\n ) {}\n\n get(\n pagedReq: PagedListRequestInterface,\n ): Observable> {\n const activatableItems$ = this.sortOptions$.pipe(\n switchMap((sortOptions) => {\n const listReq = this.buildRequest(this.config.partnerId, this.config.marketId, pagedReq, sortOptions);\n return this.listActivatableItems(listReq);\n }),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n\n const rows$ = merge(\n activatableItems$,\n activatableItems$.pipe(switchMap((response) => this.sideloadData(response))),\n );\n\n return combineLatest([rows$, this.immutableSelectedItems$, this.selectedItems$]).pipe(\n map(([rows]) => {\n rows.data = this.processActivatableItems(rows.data);\n return rows;\n }),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n private buildRequest(\n partnerId: string,\n marketId: string,\n pagedReq: PagedListRequestInterface,\n sortOptions: ListActivatableItemsSortOptions,\n ): ListActivatableItemsRequest {\n const cursor = pagedReq.pagingOptions?.cursor ?? '';\n const pageSize = pagedReq.pagingOptions?.pageSize ?? 0;\n const itemSelectorFilters = pagedReq?.filters?.[0];\n return new ListActivatableItemsRequest({\n sellerId: partnerId,\n storeId: marketId,\n cursor: cursor,\n pageSize: pageSize,\n filters: { searchTerm: pagedReq.searchOptions?.text, ...itemSelectorFilters },\n sortOptions: sortOptions,\n });\n }\n\n private listActivatableItems(\n req: ListActivatableItemsRequest,\n ): Observable> {\n return this.itemsApiService.listActivatableItems(req).pipe(\n map((response) => {\n return {\n data: (response.activatableItemSummaries as ItemSelectorRowItem[]) ?? [],\n pagingMetadata: {\n nextCursor: response.nextCursor,\n hasMore: response.hasMore,\n totalResults: response.totalResults,\n },\n };\n }),\n );\n }\n\n private sideloadData(\n rows: PagedResponseInterface,\n ): Observable> {\n const pricing$ = this.getPricing(rows.data).pipe();\n const editions$ = this.getEditions(rows).pipe(startWith([]));\n const apps$ = this.getMarketplaceApps(rows.data).pipe(startWith([]));\n return combineLatest([pricing$, editions$, apps$]).pipe(\n skipWhile(([pricing, editions, apps]) => pricing.loading && !editions?.length && !apps?.length),\n map(([pricing, editions, apps]) => this.hydrateRows(rows, pricing, editions, apps)),\n );\n }\n\n private hydrateRows(\n rows: PagedResponseInterface,\n pricing: GetItemPricingState,\n editions: ListByMultiAppResponseAppEditions[],\n apps: App[],\n ): PagedResponseInterface {\n const pricingContexts = this.getPricingContexts();\n rows.data.forEach((rowItem: ItemSelectorRowItem) => {\n if (!pricing.loading) {\n rowItem.appPrices = this.getItemPrice(rowItem.itemId, pricing.prices, pricingContexts);\n rowItem.pricesInitialized = true;\n }\n if (editions.length > 0) {\n const rowItemEditions = editions.find((edition) => edition.appId === rowItem.itemId);\n if (rowItemEditions?.editions?.length) {\n rowItem.editions = rowItemEditions.editions.map(convertToItemSelectorEdition);\n const [item] = findItem(this.itemSelectorServiceV2.allItems(), rowItem.itemId);\n const edition = findItemEdition(item, rowItemEditions.editions);\n rowItem.editionId = edition?.editionId ?? '';\n }\n }\n if (apps?.length) {\n const app = apps.find((app) => app?.key?.appId === rowItem.itemId);\n rowItem.childApps = app?.childApps ?? [];\n }\n });\n return rows;\n }\n\n private getPricing(rows: ItemSelectorRowItem[]): Observable {\n const itemIds = rows.map((item) => item.itemId);\n if (!itemIds.length || (this.config.hideWholesale && this.config.hideRetail)) {\n return of({ prices: [], loading: false });\n }\n const request = new GetMultiPricingRequest({\n itemIds: itemIds,\n pricingContexts: new FieldMask({\n paths: this.getPricingContexts(),\n }),\n partnerId: this.config.partnerId,\n marketId: this.config.marketId,\n });\n if (this.config.priceType === 'instance') {\n request.requestedApps = rows.map((row) => new RequestedApp({ appId: row.itemId, quantity: row.quantity || 1 }));\n request.filterOptions = new FieldMask({ paths: ['instance_price'] });\n }\n return this.appPricingApiService.getMultiPricing(request).pipe(\n map((resp) => ({\n prices: resp?.appPrices ?? [],\n loading: false,\n })),\n catchError(() => {\n return of({ prices: [], loading: false });\n }),\n startWith({ prices: [], loading: true }),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n private getPricingContexts(): string[] {\n const pricingContexts: string[] = [];\n if (!this.config.hideRetail) pricingContexts.push('retail');\n if (!this.config.hideWholesale) pricingContexts.push('wholesale');\n return pricingContexts;\n }\n\n private getEditions(\n rows: PagedResponseInterface,\n ): Observable {\n const appIds = rows.data.filter((item) => item.usesEditions).map((item) => item.itemId);\n if (!appIds.length) {\n return of([]);\n }\n const request = new ListByMultiAppRequest({ appIds });\n return this.editionsService.listByMultiApp(request).pipe(\n map((resp) => resp?.appEditions),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n private getMarketplaceApps(rows: ItemSelectorRowItem[]): Observable {\n const getMultiAppRequest = {\n projectionFilter: {\n paths: ['childApps'],\n },\n appKeys: rows.map((row) => {\n return {\n appId: row.itemId,\n editionId: row.editionId,\n };\n }),\n partnerId: this.config.partnerId,\n marketId: this.config.marketId,\n };\n return this.partnerApiService.getMultiApp(getMultiAppRequest).pipe(\n map((response) => response.apps),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n private processActivatableItems(activatableItems: ItemSelectorRowItem[]): ItemSelectorRowItem[] {\n return activatableItems.map((activatableItem) => {\n activatableItem = this.markActivatableItemAsSelected(activatableItem);\n activatableItem = this.markUnavailableItemInCountry(activatableItem, this.config.country);\n activatableItem = markActiveEdition(\n this.itemSelectorServiceV2.selectedItems(),\n this.itemSelectorServiceV2.immutableSelectedItems(),\n activatableItem,\n );\n return activatableItem;\n });\n }\n\n private markActivatableItemAsSelected(activatableItem: ItemSelectorRowItem): ItemSelectorRowItem {\n const [selection] = findItem(this.itemSelectorServiceV2.allItems(), activatableItem.itemId);\n activatableItem.isSelected = !!selection && (!activatableItem.isMultiactivatable || !selection.immutable);\n activatableItem.quantity = selection?.immutable ? 0 : selection?.quantity ?? 0;\n activatableItem.immutable = selection?.immutable;\n return activatableItem;\n }\n\n private markUnavailableItemInCountry(activatableItem: ItemSelectorRowItem, country?: string): ItemSelectorRowItem {\n activatableItem.unavailableInCountry = !this.isItemAvailableInCountry(activatableItem, country);\n return activatableItem;\n }\n\n private isItemAvailableInCountry(item: ItemSelectorRowItem, country?: string): boolean {\n if (!country || !item?.requiredCountries) return true;\n const matchesCountry = item?.requiredCountries?.some((c) => c.toUpperCase() === country.toUpperCase());\n return matchesCountry;\n }\n\n private getItemPrice(itemId: string, prices: AppPrices[], pricingContexts: string[]): AppPrices {\n const itemPrice = prices.find((price) => price.appId === itemId);\n if (itemPrice) return itemPrice;\n\n const unavailableItemPrice = new AppPrices({ appId: itemId, pricesForContexts: {} });\n pricingContexts.forEach((context) => {\n unavailableItemPrice.pricesForContexts[context] = new AppPrice({ appId: itemId });\n });\n return unavailableItemPrice;\n }\n}\n\ninterface GetItemPricingState {\n prices: AppPrices[];\n loading: boolean;\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\nimport { ActivatableItemSummary, DeliveryMethod, ItemType } from '@vendasta/marketplace-apps';\n\n@Pipe({ name: 'formatProductType' })\nexport class FormatProductTypePipe implements PipeTransform {\n constructor(private translateService: TranslateService) {}\n\n transform(item: ActivatableItemSummary): string {\n if (item.itemType === ItemType.ITEM_TYPE_PACKAGE) {\n return this.translateService.instant('COMMON.ITEM_TYPE.PACKAGE');\n }\n\n switch (item.deliveryMethod) {\n case DeliveryMethod.DELIVERY_METHOD_PRODUCT:\n switch (item.itemType) {\n case ItemType.ITEM_TYPE_APP:\n case ItemType.ITEM_TYPE_ADDON:\n return this.translateService.instant('COMMON.ITEM_TYPE.PRODUCT');\n default:\n return '';\n }\n case DeliveryMethod.DELIVERY_METHOD_SERVICE:\n return this.translateService.instant('COMMON.ITEM_TYPE.SERVICE');\n default:\n return '';\n }\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { ActivatableItemSummary, ItemType } from '@vendasta/marketplace-apps';\n\n@Pipe({ name: 'formatProductRequirement' })\nexport class FormatProductRequirementPipe implements PipeTransform {\n transform(item: ActivatableItemSummary): string {\n if (item?.requiredItems?.length > 0) {\n const requiredItemName = item.requiredItems[0].name;\n switch (item.itemType) {\n case ItemType.ITEM_TYPE_ADDON:\n return `Add-on to ${requiredItemName}`;\n case ItemType.ITEM_TYPE_APP:\n if (item.requiredItems.length > 1) {\n return `Requires ${requiredItemName} and ${item.requiredItems.length - 1} more`;\n }\n return `Requires ${requiredItemName}`;\n default:\n return '';\n }\n }\n return '';\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { ItemSelectorRowItem } from '../interface';\n\n@Pipe({ name: 'parseWholesaleCurrency' })\nexport class ParseWholesaleCurrencyPipe implements PipeTransform {\n transform(item: ItemSelectorRowItem): string {\n const pricesForEditions = item?.appPrices?.pricesForContexts?.['wholesale']?.pricesForEditions;\n if (pricesForEditions && Object.keys(pricesForEditions).length > 0) {\n const [firstEditionId] = Object.keys(pricesForEditions);\n const currencies = pricesForEditions[firstEditionId]?.pricesForCurrencies;\n if (currencies && Object.keys(currencies).length > 0) {\n return Object.keys(currencies)[0];\n }\n }\n return 'USD';\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { ItemSelectorRowItem } from '../interface';\n\n@Pipe({\n name: 'isItemDisabled',\n pure: false,\n})\nexport class IsItemDisabledPipe implements PipeTransform {\n transform(item: ItemSelectorRowItem, hideQuantity: boolean): boolean {\n if (item.isSuspended || item.unavailableInCountry) return true;\n if (item.isMultiactivatable && !hideQuantity) return false;\n\n const invalidEditions = item?.usesEditions && !item?.editions;\n return item?.immutable || !!invalidEditions;\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { ItemSelectorRowItem } from '../interface';\nimport { getName as getCountryNameFromCode } from 'country-list';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Pipe({\n name: 'formatDisabledItem',\n pure: true,\n})\nexport class FormatDisabledItemPipe implements PipeTransform {\n constructor(private translateService: TranslateService) {}\n\n transform(item: ItemSelectorRowItem, countryCode = '', context: Context): string {\n if (item.isSuspended) {\n return this.translateService.instant(this.translationPath(UnavailableItemType.SUSPENDED, context));\n }\n\n if (item.unavailableInCountry) {\n return this.translateService.instant(this.translationPath(UnavailableItemType.COUNTRY_RESTRICTION, context), {\n country: this.formatCountry(countryCode, context),\n });\n }\n\n if (item.isSelected && item.immutable) {\n return this.translateService.instant(this.translationPath(UnavailableItemType.ALREADY_ACTIVE, context));\n }\n\n return this.translateService.instant(this.translationPath(UnavailableItemType.DEFAULT, context));\n }\n\n translationPath(type: UnavailableItemType, context: Context) {\n const base = 'COMMON.UNAVAILABLE_ITEM';\n const contextPath = context === 'subtext' ? 'SUBTEXT' : 'TOOLTIP';\n return base + '.' + type + '.' + contextPath;\n }\n\n formatCountry(countryCode: string, context: Context) {\n if (context === 'tooltip') {\n return (\n getCountryNameFromCode(countryCode) ??\n this.translateService.instant('COMMON.UNAVAILABLE_ITEM.COUNTRY_RESTRICTION.COUNTRY_FALLBACK')\n );\n }\n return countryCode;\n }\n}\n\nexport enum UnavailableItemType {\n SUSPENDED = 'SUSPENDED',\n COUNTRY_RESTRICTION = 'COUNTRY_RESTRICTION',\n ALREADY_ACTIVE = 'ALREADY_ACTIVE',\n DEFAULT = 'DEFAULT',\n}\n\ntype Context = 'tooltip' | 'subtext';\n", "import {\n Component,\n computed,\n effect,\n EventEmitter,\n Input,\n OnChanges,\n OnDestroy,\n OnInit,\n Output,\n signal,\n SimpleChanges,\n untracked,\n ViewChild,\n} from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';\nimport { EditionsApiService } from '@galaxy/marketplace-apps/v1';\nimport { TranslateService } from '@ngx-translate/core';\nimport { OpenConfirmationModalService } from '@vendasta/galaxy/confirmation-modal';\nimport { PopoverPositions } from '@vendasta/galaxy/popover';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { GalaxyDataSource, TableContentHeaderComponent } from '@vendasta/galaxy/table';\nimport { GalaxyColumnDef } from '@vendasta/galaxy/table/src/table.interface';\nimport {\n AppPricingApiService,\n ItemsApiService,\n ItemType,\n ListActivatableItemsSortField,\n ListActivatableItemsSortOptions,\n OrderFormFilter,\n PartnerApiService,\n SortDirection,\n} from '@vendasta/marketplace-apps';\nimport { BehaviorSubject, Observable } from 'rxjs';\nimport { LineItem, LineItemCacheService } from '../services/line-item-cache.service';\nimport {\n ItemSelectorConfig,\n ItemSelectorFilters,\n ItemSelectorRowItem,\n ItemSelectorState,\n SelectedItem,\n SelectionChange,\n SelectionChangeEventType,\n} from './interface';\nimport { ITEMS_SELECTOR_COLUMNS } from './item-selector-columns';\nimport { ItemSelectorServiceV2, SelectionEvent, SelectionOptions } from './item-selector-v2.service';\nimport { ItemSelectorDataSource } from './item-selector.data-source';\nimport { convertRowItemToSelectedItem } from './util/item';\n\n@Component({\n selector: 'inventory-ui-item-selector',\n templateUrl: './item-selector.component.html',\n styleUrls: ['./item-selector.component.scss'],\n providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } }],\n})\nexport class ItemSelectorComponent implements OnInit, OnChanges, OnDestroy {\n readonly ItemType = ItemType;\n readonly PopoverPositions = PopoverPositions;\n\n @Input() config?: ItemSelectorConfig;\n protected configuration: Required = DEFAULT_CONFIG;\n\n @Input() dependenciesLoading = false;\n\n @Input() set selectedItems(value: SelectedItem[]) {\n this.itemSelectorServiceV2.initSelectedItems(value);\n }\n\n @Input() set immutableSelectedItems(value: SelectedItem[]) {\n this.itemSelectorServiceV2.initImmutableSelectedItems(value);\n }\n\n @Output() selectionChange: EventEmitter = new EventEmitter();\n\n @ViewChild('tableHeader') tableHeader!: TableContentHeaderComponent;\n columns: GalaxyColumnDef[] = ITEMS_SELECTOR_COLUMNS;\n dataSource!: ItemSelectorDataSource;\n tableDataSource!: GalaxyDataSource;\n\n private sortOptions$$ = new BehaviorSubject(\n new ListActivatableItemsSortOptions({\n sortField: ListActivatableItemsSortField.LIST_ACTIVATABLE_ITEMS_SORT_FIELD_DEFAULT_NAME,\n sortDirection: SortDirection.ASCENDING,\n }),\n );\n\n private immutableSelectedItems$ = toObservable(this.itemSelectorServiceV2.immutableSelectedItems);\n private selectedItems$ = toObservable(this.itemSelectorServiceV2.selectedItems);\n\n private state = signal({\n itemType: 'products',\n requiredItem: null,\n showAddons: true,\n showUnavailableItems: false,\n });\n\n private filters = computed(() => {\n const state = this.state();\n\n return {\n itemTypes: calculateItemTypes(state.itemType, state.showAddons),\n includeSuspended: state.showUnavailableItems,\n requiredItemId: requiredItemFilter(state.itemType, state.requiredItem),\n country: state.showUnavailableItems ? '' : this.configuration.country,\n hasOrderForm: this.configuration.hideItemsWithOrderForms\n ? OrderFormFilter.ORDER_FORM_FILTER_NO_ORDER_FORM\n : OrderFormFilter.ORDER_FORM_FILTER_UNSPECIFIED,\n };\n });\n\n public showAddons = computed(() => this.state().showAddons);\n public requiredItem = computed(() => this.state().requiredItem);\n public showUnavailableItems = computed(() => this.state().showUnavailableItems);\n public itemType = computed(() => this.state().itemType);\n public itemTypeTranslationKey = computed(() => {\n const itemType = this.itemType();\n switch (itemType) {\n case 'products': {\n return 'COMMON.ITEM_TYPE.PRODUCTS';\n }\n case 'packages': {\n return 'COMMON.ITEM_TYPE.PACKAGES';\n }\n default: {\n return 'COMMON.ITEM_TYPE.ALL_ITEMS';\n }\n }\n });\n\n constructor(\n private itemsApiService: ItemsApiService,\n private appPricingApiService: AppPricingApiService,\n private itemSelectorServiceV2: ItemSelectorServiceV2,\n private editionsService: EditionsApiService,\n private partnerApiService: PartnerApiService,\n private lineItemCacheService: LineItemCacheService,\n private confirmationModalService: OpenConfirmationModalService,\n private translateService: TranslateService,\n private snackbarService: SnackbarService,\n ) {\n effect(() => {\n this.updateTableFilters();\n });\n\n effect(() => {\n const selectedItems = this.itemSelectorServiceV2.selectedItems();\n if (this.configuration?.businessId) {\n const lineItems: LineItem[] =\n selectedItems.map((selectedItem) => {\n const editionId = selectedItem.type === 'app' ? selectedItem.edition?.editionId : undefined;\n return {\n itemId: selectedItem.itemId,\n editionId: editionId,\n itemType: selectedItem.type,\n quantity: selectedItem.quantity,\n };\n }) || [];\n this.lineItemCacheService.cacheLineItems(this.configuration.businessId, lineItems);\n }\n });\n\n effect(() => {\n const selectionEvents = this.itemSelectorServiceV2.changeEvents();\n untracked(() => {\n const selectionEvent = selectionEvents[0];\n // Don't emit for duplicate item events\n if (selectionEvent?.eventSource === 'conflict-duplicate-item' && selectionEvent?.eventType === 'deselected') {\n return;\n }\n // Emit all other events, but only show snackbar for dependency events\n if (selectionEvent?.updatedItems?.length) {\n this.emitSelectionChanges(selectionEvent);\n }\n if (selectionEvent?.eventSource === 'user') {\n return;\n }\n\n this.notifyUserOfChanges(selectionEvent);\n });\n });\n }\n\n ngOnInit(): void {\n this.dataSource = new ItemSelectorDataSource(\n this.itemsApiService,\n this.appPricingApiService,\n this.editionsService,\n this.sortOptions$$.asObservable(),\n this.itemSelectorServiceV2,\n this.immutableSelectedItems$,\n this.selectedItems$,\n this.configuration,\n this.partnerApiService,\n );\n this.tableDataSource = new GalaxyDataSource(this.dataSource);\n this.setupColumns();\n this.updateTableFilters();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes.config) {\n const config = changes.config.currentValue as ItemSelectorConfig;\n this.configuration = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n if (config?.itemTypesAvailable?.length) {\n this.setItemType(config.itemTypesAvailable[0]);\n }\n }\n }\n\n ngOnDestroy(): void {\n this.itemSelectorServiceV2.clearSelection();\n }\n\n setupColumns() {\n const hideRetailColumn = (column: GalaxyColumnDef) => column.id === 'retail' && this.config?.hideRetail;\n const hideWholesaleColumn = (column: GalaxyColumnDef) => column.id === 'wholesale' && this.config?.hideWholesale;\n\n this.columns = this.columns.filter((column) => !hideRetailColumn(column) && !hideWholesaleColumn(column));\n }\n\n updateTableFilters(): void {\n const filters = this.filters();\n this.tableDataSource?.setFilters([filters]);\n }\n\n handleSelectionChange(selected: boolean, rowItem: ItemSelectorRowItem): void {\n rowItem.quantity = selected ? 1 : 0;\n const selectedItem = convertRowItemToSelectedItem(rowItem);\n this.itemSelectorServiceV2.updateSelection(selected, selectedItem, this.selectionOptionsFromConfig());\n }\n\n handleEditionChange(editionId: string, rowItem: ItemSelectorRowItem): void {\n rowItem.editionId = editionId;\n const selectedItem = convertRowItemToSelectedItem(rowItem);\n this.itemSelectorServiceV2.updateItemEdition(selectedItem, this.selectionOptionsFromConfig());\n }\n\n handleQuantityChange(event: Event, rowItem: ItemSelectorRowItem): void {\n rowItem.quantity = (event.target as HTMLInputElement).valueAsNumber;\n const selectedItem = convertRowItemToSelectedItem(rowItem);\n this.itemSelectorServiceV2.updateItemQuantity(selectedItem, this.selectionOptionsFromConfig());\n }\n\n setShowUnavailableItems(value: boolean): void {\n this.state.update((s) => ({ ...s, showUnavailableItems: value }));\n }\n\n setShowAddons(value: boolean): void {\n this.state.update((s) => {\n s.showAddons = value;\n if (!value) {\n s.requiredItem = null;\n }\n return { ...s };\n });\n }\n\n setRequiredItem(item: ItemSelectorRowItem): void {\n this.state.update((s) => ({ ...s, requiredItem: item, showAddons: true }));\n this.tableHeader.setSearchTerm('');\n }\n\n unsetRequiredItem(): void {\n this.state.update((s) => ({ ...s, requiredItem: null }));\n this.tableHeader.setSearchTerm('');\n }\n\n private selectionOptionsFromConfig(): SelectionOptions {\n return {\n partnerId: this.configuration.partnerId,\n marketId: this.configuration.marketId,\n confirmItenComflictResolutionCallback: this.confirmItemConflictResolutionModal.bind(this),\n enforceDependencies: this.configuration.enforceDependencies,\n };\n }\n\n private confirmItemConflictResolutionModal(): Observable {\n return this.confirmationModalService.openModal({\n title: this.translateService.instant('ITEM_SELECTOR.CONFLICT_RESOLUTION_MODAL.TITLE'),\n message: this.translateService.instant('ITEM_SELECTOR.CONFLICT_RESOLUTION_MODAL.MESSAGE'),\n confirmButtonText: this.translateService.instant('COMMON.ACTIONS.ADD_TO_ORDER'),\n type: 'confirm',\n });\n }\n\n private formatItemNames(selectedItems: SelectedItem[], includeEditions = true): string {\n return selectedItems\n .map((si) => {\n let name = si.name;\n if (si.type === 'app' && includeEditions && si.edition?.name) {\n name = `${si.name} | ${si.edition.name}`;\n }\n return name;\n })\n .join(', ');\n }\n\n private emitSelectionChanges(selectionEvent: SelectionEvent): void {\n selectionEvent.updatedItems.forEach((updatedItem) => {\n this.selectionChange.emit({\n selectedItems: this.itemSelectorServiceV2.selectedItems(),\n updatedItem: updatedItem,\n eventType:\n selectionEvent.eventType === 'selected'\n ? SelectionChangeEventType.SELECTED\n : SelectionChangeEventType.DESELECTED,\n });\n });\n }\n\n setItemType(type: 'products' | 'packages') {\n this.state.update((s) => ({ ...s, itemType: type, requiredItem: null }));\n }\n\n private notifyUserOfChanges(selectionEvent: SelectionEvent): void {\n switch (selectionEvent?.eventSource) {\n case 'dependency-selection-change':\n this.showSelectionChangeNotification(selectionEvent);\n break;\n case 'dependency-edition-change':\n this.showEditionChangeNotification(selectionEvent);\n break;\n case 'dependency-unavailable':\n this.snackbarService.openErrorSnack('ITEM_SELECTOR.DEPENDENCIES.REQUIRED_ITEMS_UNAVAILABLE');\n break;\n }\n }\n\n private showSelectionChangeNotification(selectionEvent: SelectionEvent): void {\n const translation =\n selectionEvent.eventType === 'selected'\n ? 'ITEM_SELECTOR.DEPENDENCIES.SELECTION_NOTIFICATION'\n : 'ITEM_SELECTOR.DEPENDENCIES.REMOVAL_NOTIFICATION';\n const notification = this.translateService.instant(translation, {\n itemNames: this.formatItemNames(selectionEvent.updatedItems),\n });\n this.snackbarService.openSuccessSnack(notification);\n }\n\n private showEditionChangeNotification(selectionEvent: SelectionEvent): void {\n if (selectionEvent.eventType !== 'deselected' || !selectionEvent.triggerItem) {\n return;\n }\n\n const translation = 'ITEM_SELECTOR.DEPENDENCIES.REMOVAL_DUE_TO_EDITION_CHANGE';\n const notification = this.translateService.instant(translation, {\n affectedItemNames: this.formatItemNames(selectionEvent.updatedItems),\n triggerItemName: this.formatItemNames([selectionEvent.triggerItem], false),\n });\n this.snackbarService.openSuccessSnack(notification);\n }\n}\n\nexport const DEFAULT_CONFIG: Required = {\n partnerId: '',\n pageSize: 15,\n country: '',\n priceType: 'generic',\n marketId: '',\n businessId: '',\n hideOwnerName: false,\n hideWholesale: false,\n hideRetail: false,\n hideQuantity: false,\n hideItemsWithOrderForms: false,\n itemTypesAvailable: ['products'],\n enforceDependencies: false,\n};\n\nfunction calculateItemTypes(itemType: 'products' | 'packages' | 'allItems', showAddons: boolean) {\n let itemTypes = [ItemType.ITEM_TYPE_APP, ItemType.ITEM_TYPE_PACKAGE];\n\n if (itemType === 'packages') {\n return [ItemType.ITEM_TYPE_PACKAGE];\n }\n if (itemType === 'products') {\n itemTypes = [ItemType.ITEM_TYPE_APP];\n }\n\n if (showAddons) {\n itemTypes.push(ItemType.ITEM_TYPE_ADDON);\n }\n\n return itemTypes;\n}\n\nfunction requiredItemFilter(itemType: 'products' | 'packages' | 'allItems', requiredItem: ItemSelectorRowItem | null) {\n if (itemType === 'packages') {\n return '';\n }\n\n return requiredItem?.itemId ?? '';\n}\n", "\n \n
1\" filter-button class=\"filter-toggle\">\n \n \n {{ 'COMMON.ITEM_TYPE.PRODUCTS' | translate }}\n \n \n {{ 'COMMON.ITEM_TYPE.PACKAGES' | translate }}\n \n \n
\n\n
\n \n {{ 'ITEM_SELECTOR.SHOW_ADDONS' | translate }}\n \n\n \n {{ 'ITEM_SELECTOR.SHOW_UNAVAILABLE' | translate }}\n \n
\n\n
\n \n Add-ons: {{ requiredItem()?.sellerWhitelabelName ?? requiredItem()?.defaultName ?? requiredItem()?.itemId }}\n \n \n
\n \n\n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n
\n
\n\n \n
\n \n \n \n\n \n \n \n {{ 'ITEM_SELECTOR.COLUMN.NAME' | translate }}\n
\n \n
\n
\n
\n {{ item?.sellerWhitelabelName ?? item?.defaultName }}\n
\n
\n \n \n
\n
\n\n
\n \n \n \n \n \n {{ item | formatProductType }}\n \n \n \n {{\n 'ITEM_SELECTOR.ITEM_TYPE_BY_OWNER'\n | translate: { itemType: item | formatProductType, owner: item.ownerName }\n }}\n \n
\n\n
\n {{ item | formatDisabledItem: configuration.country : 'subtext' }}\n
\n\n \n 1\"\n (mouseleave)=\"productRequirementPopover.isOpen = false\"\n [glxyPopover]=\"productRequirementPopover\"\n >\n {{ item | formatProductRequirement }}\n
\n\n \n

{{ 'ITEM_SELECTOR.REQUIRED_ITEMS_LABEL' | translate }}

\n
    \n
  • \n {{ requiredItem.name }}\n
  • \n
\n
\n \n 0\"\n data-cy=\"addon-filter-chip\"\n (click)=\"setRequiredItem(item)\"\n [trackEvent]=\"{\n eventName: 'item-selector-view-addons',\n category: 'chip',\n action: 'click'\n }\"\n >\n {{ 'ITEM_SELECTOR.VIEW_ADDONS' | translate }}\n \n
\n \n
\n \n \n \n \n {{ 'ITEM_SELECTOR.COLUMN.WHOLESALE' | translate }}\n \n \n \n \n {{ 'ITEM_SELECTOR.COLUMN.RETAIL' | translate }}\n \n \n \n
\n\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatButtonToggleModule } from '@angular/material/button-toggle';\nimport { MatCheckboxModule } from '@angular/material/checkbox';\nimport { MatChipsModule } from '@angular/material/chips';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { MatSelectModule } from '@angular/material/select';\nimport { MatTableModule } from '@angular/material/table';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { BillingUiModule } from '@vendasta/billing-ui';\nimport { GalaxyAvatarModule } from '@vendasta/galaxy/avatar';\nimport { GalaxyFormFieldModule } from '@vendasta/galaxy/form-field';\nimport { GalaxyInputModule } from '@vendasta/galaxy/input';\nimport { GalaxyLoadingSpinnerModule } from '@vendasta/galaxy/loading-spinner';\nimport { GalaxyPopoverModule } from '@vendasta/galaxy/popover';\nimport { GalaxyTableModule } from '@vendasta/galaxy/table';\nimport { GalaxyTooltipModule } from '@vendasta/galaxy/tooltip';\nimport { ProductAnalyticsModule } from '@vendasta/product-analytics';\nimport { ItemCommonModule } from '../common/item-common.module';\nimport { InventoryUiI18nModule } from '../i18n/inventory-ui-i18n.module';\nimport { PackageContentsPopoverModule } from '../package-contents-popover/package-contents-popover.module';\nimport { ItemConflictService } from '../services/item-conflict.service';\nimport { ItemDependencyService } from '../services/item-dependency.service';\nimport { LineItemCacheService } from '../services/line-item-cache.service';\nimport { ItemSelectorServiceV2 } from './item-selector-v2.service';\nimport { ItemSelectorComponent } from './item-selector.component';\nimport { FormatDisabledItemPipe } from './pipes/format-disabled-item.pipe';\nimport { FormatProductRequirementPipe } from './pipes/format-product-requirement.pipe';\nimport { FormatProductTypePipe } from './pipes/format-product-type.pipe';\nimport { IsItemDisabledPipe } from './pipes/is-item-disabled.pipe';\nimport { ParseWholesaleCurrencyPipe } from './pipes/parse-wholesale-currency.pipe';\nimport { GalaxyBadgeModule } from '@vendasta/galaxy/badge';\n\nconst MAT_MODULES = [\n MatButtonModule,\n MatButtonToggleModule,\n MatCheckboxModule,\n MatChipsModule,\n MatIconModule,\n MatInputModule,\n MatMenuModule,\n MatSelectModule,\n MatTableModule,\n MatTooltipModule,\n FormsModule,\n];\n\nconst GALAXY_MODULES = [\n GalaxyTableModule,\n GalaxyAvatarModule,\n GalaxyInputModule,\n GalaxyFormFieldModule,\n GalaxyLoadingSpinnerModule,\n GalaxyPopoverModule,\n GalaxyTooltipModule,\n GalaxyBadgeModule,\n];\n\nexport const ITEM_SELECTOR_MODULE_IMPORTS = [\n ...MAT_MODULES,\n ...GALAXY_MODULES,\n BillingUiModule,\n CommonModule,\n InventoryUiI18nModule,\n ItemCommonModule,\n ProductAnalyticsModule,\n PackageContentsPopoverModule,\n TranslateModule,\n];\n\nexport const ITEM_SELECTOR_MODULE_DECLARATIONS = [\n ItemSelectorComponent,\n FormatProductTypePipe,\n FormatProductRequirementPipe,\n ParseWholesaleCurrencyPipe,\n IsItemDisabledPipe,\n FormatDisabledItemPipe,\n];\n\nexport const ITEM_SELECTOR_MODULE_PROVIDERS = [\n ItemSelectorServiceV2,\n LineItemCacheService,\n ItemConflictService,\n ItemDependencyService,\n];\n\n@NgModule({\n imports: ITEM_SELECTOR_MODULE_IMPORTS,\n exports: [ItemSelectorComponent],\n declarations: ITEM_SELECTOR_MODULE_DECLARATIONS,\n providers: ITEM_SELECTOR_MODULE_PROVIDERS,\n})\nexport class ItemSelectorModule {}\n", "import { Component, Input } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { TranslateService } from '@ngx-translate/core';\nimport { OpenConfirmationModalService } from '@vendasta/galaxy/confirmation-modal';\nimport { Observable, of } from 'rxjs';\nimport { map, switchMap, take, withLatestFrom } from 'rxjs/operators';\nimport { InventoryItem } from '../common/inventory-item';\nimport { ItemListItemRemovedEvent } from '../common/item-list/item-list.component';\nimport { InventoryListItem } from '../common/item-list/type';\nimport { SelectedItem } from '../item-selector/interface';\nimport { ItemSelectorServiceV2, SelectionOptions } from '../item-selector/item-selector-v2.service';\nimport { PackageContentsService } from '../services/package-contents.service';\n\nexport interface ItemSelectionCartConfig {\n partnerId: string;\n marketId: string;\n enforceDependencies: boolean;\n}\n\n@Component({\n selector: 'inventory-ui-item-selection-cart',\n templateUrl: './item-selection-cart.component.html',\n styleUrls: ['./item-selection-cart.component.scss'],\n})\nexport class ItemSelectionCartComponent {\n @Input() config!: ItemSelectionCartConfig;\n\n cartItems = toSignal(this.initCartItems());\n\n constructor(\n private itemSelectorService: ItemSelectorServiceV2,\n private packageContentsService: PackageContentsService,\n private confirmationModalService: OpenConfirmationModalService,\n private translateService: TranslateService,\n ) {}\n\n removeAll(): void {\n this.confirmationModalService\n .openModal({\n title: this.translateService.instant('ITEM_SELECTION_CART.CLEAR_ALL_CONFIRMATION_MODAL.TITLE'),\n message: this.translateService.instant('ITEM_SELECTION_CART.CLEAR_ALL_CONFIRMATION_MODAL.MESSAGE'),\n confirmButtonText: this.translateService.instant('COMMON.ACTIONS.CLEAR'),\n })\n .pipe(take(1))\n .subscribe((confirmed) => {\n if (!confirmed) {\n return;\n }\n this.itemSelectorService.clearSelection(true);\n });\n }\n\n removeItem(event: ItemListItemRemovedEvent): void {\n if (event.parentItemId && event.isNested) {\n const parentItemId = event.parentItemId!;\n const parentItem = this.cartItems()!.find((i) => i.itemId === event.parentItemId)!;\n if (parentItem.nestedItems!.length <= 1) {\n this.itemSelectorService.removeItemId(parentItemId, 'user', this.selectionOptionsFromConfig());\n return;\n }\n\n this.confirmAndRemoveNestedItem(event.itemId, parentItemId);\n return;\n }\n\n this.itemSelectorService.removeItemId(event.itemId, 'user', this.selectionOptionsFromConfig());\n }\n\n private confirmAndRemoveNestedItem(nestedItemId: string, parentItemId: string): void {\n const parentItem = this.cartItems()!.find((i) => i.itemId === parentItemId)!;\n this.confirmationModalService\n .openModal({\n title: this.translateService.instant('ITEM_SELECTION_CART.REMOVE_ITEM_CONFIRMATION_MODAL.TITLE'),\n message: this.translateService.instant('ITEM_SELECTION_CART.REMOVE_ITEM_CONFIRMATION_MODAL.MESSAGE', {\n parentItemName: parentItem.name,\n }),\n confirmButtonText: this.translateService.instant('COMMON.ACTIONS.REMOVE'),\n type: 'warn',\n })\n .pipe(take(1))\n .subscribe((confirmed) => {\n if (!confirmed) {\n return;\n }\n\n const remainingNestedItems = parentItem.nestedItems!.filter((ni) => ni.itemId !== nestedItemId);\n this.itemSelectorService.removeItemId(parentItemId, 'user', this.selectionOptionsFromConfig());\n remainingNestedItems.forEach((ni) =>\n this.itemSelectorService.updateSelection(true, ni, this.selectionOptionsFromConfig()),\n );\n });\n }\n\n private selectionOptionsFromConfig(): SelectionOptions {\n return {\n partnerId: this.config.partnerId,\n marketId: this.config.marketId,\n enforceDependencies: this.config.enforceDependencies,\n };\n }\n\n private initCartItems(): Observable {\n const selectedItems$ = toObservable(this.itemSelectorService.selectedItems);\n const cartItems$: Observable = selectedItems$.pipe(\n switchMap((selectedItems) => {\n const selectedPackageItems = selectedItems.filter((item) => item.type === 'package');\n const selectedPackageItemIds = selectedPackageItems.map((item) => item.itemId);\n if (!selectedPackageItemIds.length) {\n return of(new Map());\n }\n\n return this.packageContentsService.fetchMultiPackageItems(\n selectedPackageItemIds,\n this.config.partnerId,\n this.config.marketId,\n );\n }),\n withLatestFrom(selectedItems$),\n map(([packageItemsMap, selectedItems]) => this.buildCartItems(packageItemsMap, selectedItems)),\n );\n return cartItems$;\n }\n\n private buildCartItems(\n packageItemsMap: Map,\n selectedItems: SelectedItem[],\n ): InventoryListItem[] {\n const cartItems = selectedItems?.map((selectedItem) => {\n if (selectedItem.type !== 'package') {\n return selectedItem;\n }\n\n const packageItem: InventoryListItem = {\n ...selectedItem,\n };\n const nestedItems = packageItemsMap.get(selectedItem.itemId);\n if (nestedItems?.length) {\n packageItem.nestedItems = nestedItems;\n }\n return packageItem;\n });\n return cartItems ?? [];\n }\n}\n", "
\n
\n

\n {{ 'ITEM_SELECTION_CART.TITLE' | translate }}\n

\n \n \n {{ 'COMMON.ACTIONS.CLEAR' | translate }}\n \n \n
\n\n \n
\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatListModule } from '@angular/material/list';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { ItemCommonModule } from '../common/item-common.module';\nimport { ItemSelectionCartComponent } from './item-selection-cart.component';\n\nexport const ITEM_SELECTION_CART_MODULE_DECLARATIONS = [ItemSelectionCartComponent];\n\nexport const ITEM_SELECTION_CART_MODULE_IMPORTS = [\n CommonModule,\n ItemCommonModule,\n MatButtonModule,\n MatIconModule,\n MatListModule,\n TranslateModule,\n];\n\n@NgModule({\n exports: [ItemSelectionCartComponent],\n declarations: ITEM_SELECTION_CART_MODULE_DECLARATIONS,\n imports: ITEM_SELECTION_CART_MODULE_IMPORTS,\n})\nexport class ItemSelectionCartModule {}\n", "import { NgModule } from '@angular/core';\nimport { ItemConflictService } from './item-conflict.service';\nimport { ItemDependencyService } from './item-dependency.service';\nimport { LineItemCacheService } from './line-item-cache.service';\nimport { PackageContentsService } from './package-contents.service';\n\n@NgModule({\n imports: [],\n exports: [],\n declarations: [],\n providers: [LineItemCacheService, ItemConflictService, ItemDependencyService, PackageContentsService],\n})\nexport class ServicesModule {}\n", "import { CommonModule } from '@angular/common';\nimport { Component, Inject, OnInit } from '@angular/core';\nimport { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCheckboxModule } from '@angular/material/checkbox';\nimport { DateAdapter, MAT_DATE_FORMATS, MatNativeDateModule, NativeDateAdapter } from '@angular/material/core';\nimport { MatDatepickerModule } from '@angular/material/datepicker';\nimport { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { MatSelectModule } from '@angular/material/select';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { BillingUiModule } from '@vendasta/billing-ui';\nimport { GalaxyAvatarModule } from '@vendasta/galaxy/avatar';\nimport { GalaxyButtonGroupModule } from '@vendasta/galaxy/button-group';\nimport { GalaxyFormFieldModule } from '@vendasta/galaxy/form-field';\nimport { GalaxyPipesModule } from '@vendasta/galaxy/pipes';\nimport { RevenuePeriod } from '@vendasta/sales-orders';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport { Observable, combineLatest } from 'rxjs';\nimport { map, shareReplay, startWith, take } from 'rxjs/operators';\nimport { FormatRevenuePeriod } from './format-revenue-period.pipe';\nimport { RevenuePeriodToGalaxyFrequency } from './sales-to-galaxy-frequency.pipe';\n\ndayjs.extend(utc);\n\nexport interface BillingTermDialogInput {\n itemId: string;\n name: string;\n iconUrl: string;\n revenueComponent: { revenue: number; period: RevenuePeriod };\n startDate: Date;\n endDate: Date | null;\n}\n\nexport interface BillingTermDialogOutput {\n startDate: Date;\n endDate: Date | null;\n billingFrequency: RevenuePeriod;\n addAnotherTerm: boolean;\n}\n\nconst MATERIAL_IMPORTS = [\n MatDialogModule,\n MatButtonModule,\n MatInputModule,\n MatDatepickerModule,\n MatNativeDateModule,\n MatSelectModule,\n MatCheckboxModule,\n ReactiveFormsModule,\n MatMenuModule,\n MatIconModule,\n];\nconst GLXY_IMPORTS = [GalaxyAvatarModule, GalaxyFormFieldModule, GalaxyPipesModule, GalaxyButtonGroupModule];\nexport const IMPORTS = [\n CommonModule,\n ...MATERIAL_IMPORTS,\n ...GLXY_IMPORTS,\n BillingUiModule,\n TranslateModule,\n RevenuePeriodToGalaxyFrequency,\n FormatRevenuePeriod,\n];\n\nconst CUSTOM_DATE_FORMATS = {\n display: {\n dateInput: {\n // MMM DD, YYYY -> Jan 1, 2024\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n },\n monthYearLabel: { month: 'short', year: 'numeric' },\n },\n};\n\nexport const PROVIDERS = [\n { provide: DateAdapter, useClass: NativeDateAdapter },\n { provide: MAT_DATE_FORMATS, useValue: CUSTOM_DATE_FORMATS },\n];\n\ninterface State {\n estimatedEndDate: Date | null;\n}\n\ninterface TermOptions {\n label: string;\n value: boolean;\n}\n\n@Component({\n selector: 'inventory-ui-billing-term-dialog',\n standalone: true,\n imports: IMPORTS,\n providers: PROVIDERS,\n templateUrl: './billing-term-dialog.component.html',\n styleUrl: './billing-term-dialog.component.scss',\n})\nexport class BillingTermDialogComponent implements OnInit {\n protected form!: FormGroup;\n protected currentDate = new Date();\n protected estimatedEndDate$!: Observable;\n protected state$!: Observable;\n protected termOptions: TermOptions[] = [\n { label: 'EDIT_BILLING_TERMS_DIALOG.FORM.INDEFINITE_TERM', value: true },\n { label: 'EDIT_BILLING_TERMS_DIALOG.FORM.FIXED_TERM', value: false },\n ];\n protected billingFrequency!: RevenuePeriod;\n\n constructor(\n private readonly dialogRef: MatDialogRef,\n @Inject(MAT_DIALOG_DATA) protected input: BillingTermDialogInput,\n private formBuilder: FormBuilder,\n ) {}\n\n ngOnInit(): void {\n const startDate = this.input.startDate;\n let endDate = this.input.endDate;\n this.billingFrequency = this.input.revenueComponent.period;\n const isYearlyBilling = this.billingFrequency === RevenuePeriod.YEARLY;\n const endDateWithinYearlyBillingWindow = dayjs(endDate).isBefore(dayjs(startDate).add(1, 'year').toDate());\n if ((endDate && dayjs(endDate).isSame(0)) || dayjs(endDate).isBefore(dayjs(startDate))) {\n endDate = null;\n }\n if (isYearlyBilling && endDateWithinYearlyBillingWindow) {\n endDate = dayjs(startDate).add(1, 'year').toDate();\n }\n const [nBillingPeriods, billIndefinitely] = computeNBillingPeriods(startDate, endDate, this.billingFrequency);\n this.form = this.formBuilder.group({\n startDate: [startDate ?? this.currentDate],\n nBillingPeriods: [nBillingPeriods],\n billingTerm: [billIndefinitely],\n });\n this.estimatedEndDate$ = this.initEstimatedEndDate(\n startDate ?? this.currentDate,\n nBillingPeriods,\n billIndefinitely,\n );\n this.state$ = this.estimatedEndDate$.pipe(\n map((estimatedEndDate) => {\n return { estimatedEndDate: estimatedEndDate };\n }),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n\n if (this.billingFrequency === RevenuePeriod.ONETIME) {\n this.disableNBillingPeriods();\n this.disableBillIndefinitely();\n return;\n }\n if (billIndefinitely) {\n this.disableNBillingPeriods();\n }\n }\n\n save(addAnotherTerm = false): void {\n this.estimatedEndDate$.pipe(take(1)).subscribe((endDate) => {\n const output: BillingTermDialogOutput = {\n startDate: new Date(this.form.controls.startDate.value),\n endDate: endDate,\n billingFrequency: this.billingFrequency,\n addAnotherTerm: addAnotherTerm,\n };\n this.dialogRef.close(output);\n });\n }\n\n private initEstimatedEndDate(\n initStartDate: Date,\n initNBillingPeriods: number,\n initBillIndefinitely: boolean,\n ): Observable {\n const startDate$ = this.form.controls.startDate.valueChanges.pipe(startWith(initStartDate));\n const nBillingPeriods$ = this.form.controls.nBillingPeriods.valueChanges.pipe(startWith(initNBillingPeriods));\n const billIndefinitely$ = this.form.controls.billingTerm.valueChanges.pipe(startWith(initBillIndefinitely));\n return combineLatest([startDate$, nBillingPeriods$, billIndefinitely$]).pipe(\n map(([startDate, nBillingPeriods, billIndefinitely]) => {\n if (this.billingFrequency === RevenuePeriod.ONETIME || billIndefinitely) {\n return null;\n }\n if (this.billingFrequency === RevenuePeriod.YEARLY) {\n return dayjs(startDate).add(nBillingPeriods, 'year').toDate();\n }\n return dayjs(startDate).add(nBillingPeriods, 'month').toDate();\n }),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n protected handleBillingTermChange(billIndefinitely: boolean): void {\n if (!billIndefinitely) {\n this.enableNBillingPeriods();\n } else {\n this.disableNBillingPeriods();\n }\n }\n\n private enableNBillingPeriods(): void {\n this.form.controls.nBillingPeriods.patchValue(1);\n this.form.controls.nBillingPeriods.enable();\n this.form.controls.nBillingPeriods.updateValueAndValidity();\n }\n\n private disableNBillingPeriods(): void {\n this.form.controls.nBillingPeriods.patchValue('');\n this.form.controls.nBillingPeriods.disable();\n this.form.controls.nBillingPeriods.updateValueAndValidity();\n }\n\n private disableBillIndefinitely(): void {\n this.form.controls.billingTerm.patchValue(false);\n this.form.controls.billingTerm.disable();\n this.form.controls.billingTerm.updateValueAndValidity();\n }\n}\n\nfunction computeNBillingPeriods(\n startDate: Date,\n endDate: Date | null,\n billingFrequency: RevenuePeriod,\n): [number, boolean] {\n if (!startDate) {\n return [1, false];\n }\n if (startDate && !endDate) {\n return [1, true]; // Bill indefinitely\n }\n\n const parsedStartDate = dayjs(startDate);\n const parsedEndDate = dayjs(endDate);\n if (billingFrequency === RevenuePeriod.YEARLY) {\n return [parsedEndDate.diff(parsedStartDate, 'year'), false];\n }\n return [parsedEndDate.diff(parsedStartDate, 'month'), false];\n}\n", "@if (state$ | async; as state) {\n

{{ 'EDIT_BILLING_TERMS_DIALOG.TITLE' | translate }}

\n \n
\n
\n
\n \n {{ input.name }}\n
\n \n
\n\n \n {{ 'EDIT_BILLING_TERMS_DIALOG.FORM.BILLING_TERMS' | translate }}\n \n \n {{ option.label | translate }}\n \n \n \n\n \n {{ 'EDIT_BILLING_TERMS_DIALOG.FORM.START_DATE' | translate }}\n \n \n \n \n\n \n \n \n {{ 'EDIT_BILLING_TERMS_DIALOG.FORM.TERM_LENGTH' | translate }}\n \n \n \n @if (state.estimatedEndDate) {\n {{\n 'EDIT_BILLING_TERMS_DIALOG.FORM.ESTIMATED_END_DATE'\n | translate: { estimatedEndDate: state.estimatedEndDate | glxyDate }\n }}\n } @else {\n {{\n 'EDIT_BILLING_TERMS_DIALOG.FORM.ESTIMATED_END_DATE'\n | translate: { estimatedEndDate: 'EDIT_BILLING_TERMS_DIALOG.FORM.NOT_APPLICABLE' | translate }\n }}\n }\n \n \n \n
\n
\n \n \n \n \n \n \n \n \n \n \n \n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { RevenuePeriod } from '@vendasta/sales-orders';\n\n@Pipe({ name: 'formatRevenuePeriod', standalone: true })\nexport class FormatRevenuePeriod implements PipeTransform {\n transform(value: RevenuePeriod, termLength: number): string {\n return formatRevenuePeriod(value, termLength);\n }\n}\n\nexport function formatRevenuePeriod(value: RevenuePeriod, termLength: number): string {\n switch (value) {\n case RevenuePeriod.ONETIME:\n return 'EDIT_BILLING_TERMS_DIALOG.FORM.BILLING_FREQUENCY_ONE_TIME';\n case RevenuePeriod.YEARLY:\n if (termLength === 1) {\n return 'EDIT_BILLING_TERMS_DIALOG.FORM.BILLING_FREQUENCY_YEAR';\n } else return 'EDIT_BILLING_TERMS_DIALOG.FORM.BILLING_FREQUENCY_YEARS';\n default:\n if (termLength === 1) {\n return 'EDIT_BILLING_TERMS_DIALOG.FORM.BILLING_FREQUENCY_MONTH';\n } else return 'EDIT_BILLING_TERMS_DIALOG.FORM.BILLING_FREQUENCY_MONTHS';\n }\n}\n", "import { Component, Inject } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { ItemSelectorConfig, SelectedItem } from '../interface';\nimport { ItemSelectorServiceV2 } from '../item-selector-v2.service';\n\n@Component({\n templateUrl: './item-selector-dialog.component.html',\n styleUrl: './item-selector-dialog.component.scss',\n})\nexport class ItemSelectorDialogComponent {\n protected config: ItemSelectorConfig;\n protected selectedItems: SelectedItem[];\n\n constructor(\n public dialogRef: MatDialogRef,\n @Inject(MAT_DIALOG_DATA) public data: any,\n private itemSelectorService: ItemSelectorServiceV2,\n ) {\n this.config = data.config;\n this.selectedItems = data.selectedItems;\n }\n\n onSelect() {\n const selectedItems = this.itemSelectorService.selectedItems();\n this.dialogRef.close({ selectedItems: selectedItems });\n }\n}\n", "

{{ 'COMMON.ACTIONS.ADD_ITEMS' | translate }}

\n\n
\n \n
\n
\n\n \n \n\n", "import { Injectable, Signal, signal, WritableSignal } from '@angular/core';\nimport _ from 'lodash';\nimport { Observable } from 'rxjs';\nimport { InventoryItemWithPricing } from '../common/inventory-item';\nimport { ItemRow } from '../item-pricing-table/item-pricing-table.interface';\nimport { CreateLineItemFromItemRow } from '../item-pricing-table/line-item';\n\nexport type ChangedInventoryItem = InventoryItemWithPricing & { index: number };\n\nexport interface BillingTermCallbackOutput {\n startDate: Date;\n endDate?: Date;\n addAnotherTerm?: boolean;\n}\n\nexport type BillingTermCallbackFn = (editing?: boolean) => Observable;\n\nexport interface ItemsChangedEvent {\n items: ChangedInventoryItem[];\n type: ItemsChangedEventType;\n}\n\nexport type ItemsChangedEventType = 'add' | 'update' | 'delete';\n\n@Injectable()\nexport class ItemPricingTableSelectorService {\n private _items: WritableSignal = signal([]);\n items: Signal = this._items.asReadonly();\n private _events: WritableSignal = signal([]);\n events: Signal = this._events.asReadonly();\n\n setItems(items: InventoryItemWithPricing[]): void {\n if (!items?.length) {\n return;\n }\n\n this._items.set(items);\n }\n\n addItems(newItems: InventoryItemWithPricing[]): void {\n if (!newItems?.length) {\n return;\n }\n\n let lastItemIndex = this._items().length - 1;\n const changedItems = newItems.map((item) => {\n lastItemIndex += 1;\n return { ...item, index: lastItemIndex };\n });\n this.updateItems((items) => items.concat(newItems), changedItems, 'add');\n }\n\n addItemAtIndex(newItem: InventoryItemWithPricing, index: number): void {\n const nItems = this._items().length;\n if (!this.isItemIndexValid(index) && index !== nItems) {\n return;\n }\n\n this.updateItems(\n (items) => {\n items.splice(index, 0, newItem);\n return items;\n },\n [{ ...newItem, index }],\n 'add',\n );\n }\n\n setItemMatchingRow(changedItem: InventoryItemWithPricing, itemRow: ItemRow): void {\n const index = itemRow.index ?? -1;\n if (!this.isItemIndexValid(index)) {\n return;\n }\n\n this.updateItems(\n (items) => {\n items[index] = changedItem;\n return items;\n },\n [{ ...changedItem, index }],\n 'update',\n );\n }\n\n setItemsMatchingRows(changedItems: InventoryItemWithPricing[], itemRows: ItemRow[]): void {\n if (changedItems.length !== itemRows.length) {\n return;\n }\n\n this.updateItems(\n (items) => {\n changedItems.forEach((changedItem, i) => {\n const itemRow = itemRows[i];\n const index = itemRow.index ?? -1;\n if (!this.isItemIndexValid(index)) {\n return;\n }\n\n items[index] = changedItem;\n });\n return items;\n },\n changedItems.map((changedItem, i) => {\n const itemRow = itemRows[i];\n return {\n ...changedItem,\n pricing: CreateLineItemFromItemRow(itemRow),\n index: itemRow.index ?? -1,\n };\n }),\n 'update',\n );\n }\n\n removeItemMatchingRow(itemRow: ItemRow): void {\n const index = itemRow.index ?? -1;\n if (!this.isItemIndexValid(index)) {\n return;\n }\n\n const changedItem = this.getItemWithIndex(index);\n this.updateItems(\n (items) => {\n items.splice(index, 1);\n return items;\n },\n [changedItem],\n 'delete',\n );\n }\n\n addNewBillingTermForItemMatchingRow(itemRow: ItemRow, billingTermCallback: BillingTermCallbackFn): void {\n const index = itemRow.index ?? -1;\n if (!this.isItemIndexValid(index)) {\n return;\n }\n\n const item = this.getItemWithIndex(index);\n const itemCopy = structuredClone(item);\n const indexForCopy = index + 1;\n billingTermCallback().subscribe((output) => {\n if (!output) {\n return;\n }\n\n if (!itemCopy.pricing) itemCopy.pricing = {};\n if (!itemCopy.pricing.billingPeriod) itemCopy.pricing.billingPeriod = {};\n itemCopy.pricing.billingPeriod.startDate = output.startDate;\n itemCopy.pricing.billingPeriod.endDate = output.endDate ?? undefined;\n\n this.addItemAtIndex(itemCopy, indexForCopy);\n\n if (output.addAnotherTerm) {\n const itemRowCopy = structuredClone(itemRow);\n itemRowCopy.index = indexForCopy;\n this.addNewBillingTermForItemMatchingRow(itemRowCopy, billingTermCallback);\n }\n });\n }\n\n editBillingTermForItemMatchingRow(itemRow: ItemRow, billingTermCallback: BillingTermCallbackFn): void {\n const index = itemRow.index ?? -1;\n if (!this.isItemIndexValid(index)) {\n return;\n }\n\n const item = this.getItemWithIndex(index);\n billingTermCallback(true).subscribe((output) => {\n if (!output) {\n return;\n }\n\n this.setItemMatchingRow(\n {\n ...item,\n pricing: {\n billingPeriod: {\n startDate: output.startDate,\n endDate: output.endDate ?? undefined,\n },\n },\n },\n itemRow,\n );\n\n if (output.addAnotherTerm) {\n this.addNewBillingTermForItemMatchingRow(itemRow, billingTermCallback);\n }\n });\n }\n\n updateItemsWithPricing(itemRows: ItemRow[]): void {\n const items = this._items();\n const changedItems: InventoryItemWithPricing[] = [];\n const changedRows: ItemRow[] = [];\n items.forEach((item, index) => {\n const itemRow = itemRows[index];\n const lineItem = CreateLineItemFromItemRow(itemRow);\n if (!_.isEqual(item.pricing, lineItem)) {\n item.pricing = lineItem;\n changedItems.push(item);\n changedRows.push(itemRow);\n }\n });\n if (!changedItems.length || !changedRows.length) {\n return;\n }\n this.setItemsMatchingRows(changedItems, changedRows);\n }\n\n private isItemIndexValid(index: number): boolean {\n return index >= 0 && index < this._items().length;\n }\n\n private getItemWithIndex(index: number): ChangedInventoryItem {\n const item = this._items()[index];\n return {\n ...item,\n index,\n };\n }\n\n private updateItems(\n updateFn: UpdateItemsFn,\n changedItems: ChangedInventoryItem[],\n eventType: ItemsChangedEventType,\n ): void {\n // Create a new array because change detection doens't detect a change to an array's items, only the reference.\n this._items.update((items) => [...updateFn(items)]);\n this._events.update((events) => [...events, { items: changedItems, type: eventType }]);\n }\n}\n\ntype UpdateItemsFn = (items: InventoryItemWithPricing[]) => InventoryItemWithPricing[];\n", "import { Frequency } from '@vendasta/galaxy/frequency';\nimport { BillingPeriod, Cost, LineItem, Revenue, RevenueComponent, RevenuePeriod } from '@vendasta/sales-orders';\nimport { ItemRow } from './item-pricing-table.interface';\n\nexport function CreateLineItemFromItemRow(item: ItemRow): LineItem {\n return UpdateLineItemFromItemRow(new LineItem(), item);\n}\n\nexport function UpdateLineItemFromItemRow(lineItem: LineItem, item: ItemRow): LineItem {\n if (item?.wholesalePricing?.usesVariablePricing) {\n lineItem.cost = new Cost({\n customPrice: item.wholesalePricing.frequencyPrices[0].price,\n });\n }\n\n const originalRevenueComponents = lineItem.currentRevenue?.revenueComponents || [];\n const revenueComponents: RevenueComponent[] = [];\n if (item?.retailPricing) {\n revenueComponents.push(\n new RevenueComponent({\n value: item.retailPricing.price,\n period: convertFrequencyToRevenuePeriod(item.retailPricing.frequency),\n isStartingRevenue: originalRevenueComponents?.[0]?.isStartingRevenue,\n }),\n );\n }\n if (item?.fees?.length) {\n item.fees\n .filter((fee) => Boolean(fee.retailAmount))\n .forEach((fee) => {\n revenueComponents.push(new RevenueComponent({ value: fee.retailAmount, period: RevenuePeriod.ONETIME }));\n });\n }\n lineItem.currentRevenue = new Revenue({\n revenueComponents: revenueComponents,\n });\n\n lineItem.quantity = item?.quantity;\n if (item?.billingTerm) {\n lineItem.billingPeriod = new BillingPeriod({\n startDate: item.billingTerm?.startDate,\n endDate: item.billingTerm?.endDate,\n });\n }\n\n return lineItem;\n}\n\nexport function LineItemMatchesItemRow(lineItem: LineItem, item: ItemRow): boolean {\n const itemId = item?.itemId || '';\n const editionId = item.type === 'app' ? item?.edition?.editionId || '' : '';\n const lineItemAppId = lineItem?.appKey?.appId || '';\n const lineItemEditionId = lineItem?.appKey?.editionId || '';\n const lineItemPackageId = lineItem?.packageId || '';\n return (lineItemAppId === itemId && lineItemEditionId === editionId) || lineItemPackageId === itemId;\n}\n\nfunction convertFrequencyToRevenuePeriod(frequency: Frequency): RevenuePeriod {\n switch (frequency) {\n case Frequency.DAILY:\n return RevenuePeriod.DAILY;\n case Frequency.WEEKLY:\n return RevenuePeriod.WEEKLY;\n case Frequency.BI_WEEKLY:\n return RevenuePeriod.BIWEEKLY;\n case Frequency.ONE_TIME:\n return RevenuePeriod.ONETIME;\n case Frequency.MONTHLY:\n return RevenuePeriod.MONTHLY;\n case Frequency.YEARLY:\n return RevenuePeriod.YEARLY;\n default:\n return RevenuePeriod.ONETIME;\n }\n}\n", "import { Component, effect, EventEmitter, Input, Output, Signal, untracked, ViewChild } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { map, take } from 'rxjs/operators';\nimport {\n BillingTermDialogComponent,\n BillingTermDialogInput,\n} from '../billing-term-dialog/billing-term-dialog.component';\nimport { galaxyFrequencyToRevenuePeriod } from '../billing-term-dialog/sales-to-galaxy-frequency.pipe';\nimport { InventoryItemWithPricing } from '../common/inventory-item';\nimport { ItemPricingTableComponent, ItemPricingTableConfig } from '../item-pricing-table/item-pricing-table.component';\nimport { ItemRow, ItemRowChangeEvent } from '../item-pricing-table/item-pricing-table.interface';\nimport { ItemSelectorConfig } from '../item-selector/interface';\nimport { ItemSelectorDialogComponent } from '../item-selector/item-selector-dialog/item-selector-dialog.component';\nimport {\n BillingTermCallbackFn,\n ItemPricingTableSelectorService,\n ItemsChangedEvent,\n} from './item-pricing-table-selector.service';\n\nexport interface ItemPricingTableSelectorConfig {\n itemSelectorConfig: ItemSelectorConfig;\n itemPricingTableConfig: ItemPricingTableConfig;\n}\n\n@Component({\n selector: 'inventory-ui-item-pricing-table-selector',\n templateUrl: './item-pricing-table-selector.component.html',\n styleUrl: './item-pricing-table-selector.component.scss',\n})\nexport class ItemPricingTableSelectorComponent {\n @Input() config?: ItemPricingTableSelectorConfig;\n @Input()\n set selectedItems(items: InventoryItemWithPricing[]) {\n this.service.setItems(items);\n }\n\n @Output() itemsChanged = new EventEmitter();\n\n @ViewChild('itemPricingTable') itemPricingTable!: ItemPricingTableComponent;\n\n items: Signal;\n events: Signal;\n\n constructor(\n private service: ItemPricingTableSelectorService,\n private dialog: MatDialog,\n ) {\n this.items = this.service.items;\n this.events = this.service.events;\n\n effect(() => {\n const ptItems = this.itemPricingTable.getItems();\n untracked(() => this.service.updateItemsWithPricing(ptItems));\n });\n\n effect(() => {\n const events = this.events();\n const nEvents = events.length;\n if (nEvents) {\n const event = events[nEvents - 1];\n this.itemsChanged.emit(event);\n }\n });\n }\n\n onAddItems(): void {\n this.dialog\n .open(ItemSelectorDialogComponent, {\n data: {\n config: this.config?.itemSelectorConfig,\n },\n width: '1000px',\n maxWidth: '95vw',\n height: '900px',\n minHeight: '420px',\n maxHeight: '85vh',\n })\n .afterClosed()\n .pipe(\n take(1),\n map((output) => output?.selectedItems ?? []),\n )\n .subscribe((newItems) => this.service.addItems(newItems));\n }\n\n onUpdatedItemRow(changeEvent: ItemRowChangeEvent): void {\n const itemRow = changeEvent.item;\n switch (changeEvent.action) {\n case 'add':\n this.service.addItems([itemRow]);\n break;\n case 'delete':\n this.service.removeItemMatchingRow(itemRow);\n break;\n case 'add-billing-term':\n this.service.addNewBillingTermForItemMatchingRow(itemRow, this.openBillingTermDialog(itemRow));\n break;\n case 'edit-billing-term':\n this.service.editBillingTermForItemMatchingRow(itemRow, this.openBillingTermDialog(itemRow));\n break;\n }\n }\n\n private openBillingTermDialog(itemRow: ItemRow): BillingTermCallbackFn {\n return (editing?: boolean) => {\n let billingTerm = itemRow.billingTerm;\n if (!editing) {\n billingTerm = undefined;\n }\n return this.dialog\n .open(BillingTermDialogComponent, {\n data: {\n itemId: itemRow.itemId,\n name: itemRow.name,\n iconUrl: itemRow.iconUrl,\n revenueComponent: {\n revenue: itemRow.retailPricing.total,\n period: galaxyFrequencyToRevenuePeriod(itemRow.retailPricing.frequency),\n },\n startDate: billingTerm?.startDate,\n endDate: billingTerm?.endDate,\n } as BillingTermDialogInput,\n maxWidth: '90vw',\n maxHeight: '90vh',\n width: '500px',\n })\n .afterClosed()\n .pipe(take(1));\n };\n }\n}\n", "\n \n
\n \n
\n
\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialogModule } from '@angular/material/dialog';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { ItemSelectorModule } from '../item-selector.module';\nimport { ItemSelectorDialogComponent } from './item-selector-dialog.component';\n\nexport const ITEM_SELECTOR_DIALOG_MODULE_IMPORTS = [\n CommonModule,\n MatDialogModule,\n MatButtonModule,\n ItemSelectorModule,\n LexiconModule,\n TranslateModule,\n];\nexport const ITEM_SELECTOR_DIALOG_MODULE_DECLARATIONS = [ItemSelectorDialogComponent];\n\n@NgModule({\n imports: ITEM_SELECTOR_DIALOG_MODULE_IMPORTS,\n exports: [ItemSelectorDialogComponent],\n declarations: ITEM_SELECTOR_DIALOG_MODULE_DECLARATIONS,\n})\nexport class ItemSelectorDialogModule {}\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { ItemPricingTableModule } from '../item-pricing-table/item-pricing-table.module';\nimport { ItemSelectorDialogModule } from '../item-selector/item-selector-dialog/item-selector-dialog.module';\nimport { ItemPricingTableSelectorComponent } from './item-pricing-table-selector.component';\nimport { ItemPricingTableSelectorService } from './item-pricing-table-selector.service';\n\nexport const ITEM_PRICING_TABLE_SELECTOR_IMPORTS = [\n CommonModule,\n ItemPricingTableModule,\n ItemSelectorDialogModule,\n MatButtonModule,\n LexiconModule,\n TranslateModule,\n];\nexport const ITEM_PRICING_TABLE_SELECTOR_DECLARATIONS = [ItemPricingTableSelectorComponent];\nexport const ITEM_PRICING_TABLE_SELECTOR_PROVIDERS = [ItemPricingTableSelectorService];\n\n@NgModule({\n imports: ITEM_PRICING_TABLE_SELECTOR_IMPORTS,\n exports: [ItemPricingTableSelectorComponent],\n declarations: ITEM_PRICING_TABLE_SELECTOR_DECLARATIONS,\n providers: ITEM_PRICING_TABLE_SELECTOR_PROVIDERS,\n})\nexport class ItemPricingTableSelectorModule {}\n", "import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';\nimport { UntypedFormControl } from '@angular/forms';\nimport { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';\nimport { Observable } from 'rxjs';\nimport { filter, map } from 'rxjs/operators';\nimport { Config, SelectedDropdownItem, TemplateState } from '../interface';\nimport { ItemSearchSelectFilterService } from '../item-search-select-filter.service';\nimport { ItemType } from '@vendasta/marketplace-apps';\n\n@Component({\n selector: 'inventory-ui-item-search-multi-select-filter',\n templateUrl: './item-search-multi-select-filter.component.html',\n styleUrls: ['./item-search-multi-select-filter.component.scss'],\n providers: [ItemSearchSelectFilterService],\n})\nexport class ItemSearchMultiSelectFilterComponent implements OnInit {\n @Input() config: Config = { partnerId: '' };\n\n @Input() set selectedItems(value: SelectedDropdownItem[]) {\n // if the app or addon is already in the list, don't add it (as it's info has already been added, eg name)\n this.itemSearchSelectFilterService.updateSelection(value);\n }\n\n @Output() selectionChange: EventEmitter = new EventEmitter();\n\n @ViewChild('chipInput') chipInput?: ElementRef;\n\n templateState$?: Observable;\n searchCtrl = new UntypedFormControl('');\n\n constructor(private readonly itemSearchSelectFilterService: ItemSearchSelectFilterService) {}\n\n ngOnInit(): void {\n this.templateState$ = this.itemSearchSelectFilterService.init(\n this.searchCtrl.valueChanges.pipe(\n filter((term) => typeof term === 'string'),\n map((searchTerm) => searchTerm ?? ''),\n ),\n this.config,\n false,\n );\n }\n\n addItem(event: MatAutocompleteSelectedEvent, selectedItems: SelectedDropdownItem[]): void {\n const newlySelectedItem = event?.option?.value as SelectedDropdownItem;\n if (!newlySelectedItem) return;\n\n const isAlreadySelected = selectedItems.some(\n (i) => i.appId === newlySelectedItem.appId && i.addonId === newlySelectedItem.addonId,\n );\n if (isAlreadySelected) return;\n\n selectedItems = selectedItems.concat([newlySelectedItem]);\n this.resetInput();\n this.selectedItems = selectedItems;\n this.selectionChange.emit(selectedItems);\n }\n\n resetInput(): void {\n if (this.chipInput) {\n this.chipInput.nativeElement.value = '';\n }\n this.searchCtrl.setValue('');\n }\n\n removeItem(removedItem: SelectedDropdownItem, selectedItems: SelectedDropdownItem[]): void {\n selectedItems = selectedItems.filter((ra) => {\n if (this.config.filters?.itemType === ItemType.ITEM_TYPE_ADDON) {\n return ra.addonId !== removedItem.addonId;\n }\n return ra.appId !== removedItem.appId;\n });\n this.selectedItems = selectedItems;\n this.selectionChange.emit(selectedItems);\n }\n\n loadMore(): void {\n this.itemSearchSelectFilterService.triggerLoadMore();\n }\n\n displayFn = (value: unknown): string => {\n if (this.isDropdownItem(value) && value.name) {\n return value.name;\n }\n\n // fallback to first item's name\n return this.selectedItems?.length > 0 ? this.selectedItems[0]?.name ?? '' : '';\n };\n\n isDropdownItem(data: unknown): data is SelectedDropdownItem {\n if (typeof data !== 'object' || !data) return false;\n return 'appId' in data || 'addonId' in data;\n }\n}\n", "\n \n {{ 'ITEM_SEARCH_SELECT_FILTER.SEARCH' | translate }}\n \n
\n \n \n {{ requiredApp.name }}\n \n \n \n \n arrow_drop_down\n \n \n
\n {{ appSummary.name }}\n
\n
\n \n \n
\n
\n\n \n \n \n \n arrow_drop_down\n \n \n
\n
\n", "import { NgModule } from '@angular/core';\nimport { ItemSearchMultiSelectFilterComponent } from './item-search-multi-select-filter/item-search-multi-select-filter.component';\nimport { ItemSearchSingleSelectFilterComponent } from './item-search-single-select-filter/item-search-single-select-filter.component';\nimport { MatInputModule } from '@angular/material/input';\nimport { GalaxyWrapModule } from '@vendasta/galaxy/galaxy-wrap';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatChipsModule } from '@angular/material/chips';\nimport { GalaxyInfiniteScrollTriggerModule } from '@vendasta/galaxy/infinite-scroll-trigger';\nimport { GalaxyFormFieldModule } from '@vendasta/galaxy/form-field';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { InventoryUiI18nModule } from '../i18n/inventory-ui-i18n.module';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { MatAutocompleteModule } from '@angular/material/autocomplete';\nimport { ReactiveFormsModule } from '@angular/forms';\nimport { CommonModule } from '@angular/common';\nimport { DisplayLabelPipe } from './display-label.pipe';\nimport { ItemSearchSelectFilterService } from './item-search-select-filter.service';\n\n@NgModule({\n declarations: [ItemSearchMultiSelectFilterComponent, ItemSearchSingleSelectFilterComponent, DisplayLabelPipe],\n imports: [\n CommonModule,\n MatInputModule,\n MatIconModule,\n MatChipsModule,\n GalaxyInfiniteScrollTriggerModule,\n GalaxyWrapModule,\n GalaxyFormFieldModule,\n InventoryUiI18nModule,\n TranslateModule,\n MatAutocompleteModule,\n ReactiveFormsModule,\n MatFormFieldModule,\n ],\n providers: [ItemSearchSelectFilterService],\n exports: [ItemSearchMultiSelectFilterComponent, ItemSearchSingleSelectFilterComponent],\n})\nexport class ItemSearchSelectFilterModule {}\n", "import { Addon } from '@galaxy/marketplace-apps/v1';\nimport { AppKey } from '@vendasta/marketplace-apps';\nimport { SharedMarketingInformation } from '@vendasta/marketplace-apps';\nimport { GenericStoreItem, StoreProduct, StoreResellerItem } from './store';\n\nexport type GenericItem = GenericStoreItem | Addon | GenericProduct;\n\nexport interface GenericProduct {\n appId: string;\n addonId?: string;\n name: string;\n icon: string;\n tagline?: string;\n}\n\nexport function applyItemWhitelabel(\n item: T,\n whitelabel: SharedMarketingInformation,\n parentWhitelabel?: SharedMarketingInformation | null,\n): T {\n if (isProduct(item)) {\n item.name = whitelabel.name;\n item.iconUrl = whitelabel.iconUrl;\n item.headerImageUrl = whitelabel.bannerImageUrl;\n item.tagline = whitelabel.tagline;\n }\n\n if (isAddon(item)) {\n if (parentWhitelabel) {\n item.productName = parentWhitelabel.name;\n item.productIcon = parentWhitelabel.iconUrl;\n }\n item.title = whitelabel.name;\n item.icon = whitelabel.iconUrl;\n item.tagline = whitelabel.tagline;\n item.bannerImage = whitelabel.bannerImageUrl;\n }\n\n if (isResellerItem(item)) {\n item.name = whitelabel.name;\n item.icon = whitelabel.iconUrl;\n item.headerImage = whitelabel.bannerImageUrl;\n item.tagline = whitelabel.tagline;\n }\n\n if (isGenericProduct(item)) {\n item.name = whitelabel.name;\n item.icon = whitelabel.iconUrl;\n if ('tagline' in item) {\n item.tagline = whitelabel.tagline;\n }\n }\n\n return item;\n}\n\nexport function isProduct(item: GenericItem): item is StoreProduct {\n return 'productId' in item;\n}\n\nexport function isAddon(item: GenericItem): item is Addon {\n return 'addonId' in item && 'productName' in item;\n}\n\nexport function isResellerItem(item: GenericItem): item is StoreResellerItem {\n return ('appId' in item || 'addonId' in item) && 'headerImage' in item;\n}\n\nexport function isGenericProduct(item: GenericItem): item is GenericProduct {\n return ('appId' in item || 'addonId' in item) && !('headerImage' in item) && !('bannerImage' in item);\n}\n\nexport function getItemKey(item: GenericItem): AppKey {\n if (isProduct(item)) {\n return new AppKey({ appId: item.productId, editionId: item.editionId });\n }\n\n if (isAddon(item)) {\n return new AppKey({ appId: item.addonId });\n }\n\n if (isResellerItem(item)) {\n return new AppKey({ appId: item.addonId ?? item.appId, editionId: item.editionId });\n }\n\n if (isGenericProduct(item)) {\n return new AppKey({ appId: item.addonId ?? item.appId });\n }\n\n throw new Error('Failed to get AppKey from item: Unknown item type');\n}\n\nexport function getItemKeys(items: GenericItem[]): AppKey[] {\n return items.map((item) => getItemKey(item));\n}\n", "import { Inject, Injectable, OnDestroy } from '@angular/core';\nimport { AppKey, GetMultiAppRequest, PartnerApiService, SharedMarketingInformation } from '@vendasta/marketplace-apps';\nimport { distinctUntilChanged, map, Observable, of, Subscription, switchMap } from 'rxjs';\nimport { shareReplay } from 'rxjs/operators';\nimport { applyItemWhitelabel, GenericItem, getItemKey, getItemKeys, isAddon } from './item-whitelabel';\n\n@Injectable({\n providedIn: 'root',\n})\nexport class ItemWhitelabelService implements OnDestroy {\n private partnerIdSubscription: Subscription;\n private itemWhitelabelMap = new Map>();\n\n constructor(\n @Inject('PARTNER_ID') readonly partnerId$: Observable,\n private partnerMarketplaceService: PartnerApiService,\n ) {\n this.partnerIdSubscription = this.partnerId$.pipe(distinctUntilChanged()).subscribe(() => {\n this.itemWhitelabelMap = new Map>();\n });\n }\n\n ngOnDestroy(): void {\n this.partnerIdSubscription.unsubscribe();\n }\n\n public whitelabelItems$(items: T[], marketID: string): Observable {\n return this.loadItemWhitelabelMap$(getItemKeys(items), marketID).pipe(\n map((whitelabelMap) => {\n return items.map((i) => {\n const w = whitelabelMap.get(getItemKey(i).appId);\n if (w) {\n const parentWhitelabel = isAddon(i) ? whitelabelMap.get(i.appId) : null;\n return applyItemWhitelabel(i, w, parentWhitelabel);\n }\n return i;\n });\n }),\n );\n }\n\n private loadItemWhitelabelMap$(\n appKeys: AppKey[],\n marketID: string,\n ): Observable> {\n marketID = marketID ?? 'default';\n\n // use existing map if all specified items are cached\n const marketCache = this.getMarketCache(marketID);\n if (appKeys.every((appKey) => marketCache.has(appKey.appId))) {\n return of(marketCache);\n }\n\n return this.partnerId$.pipe(\n switchMap((partnerId) => {\n return this.partnerMarketplaceService.getMultiApp(\n new GetMultiAppRequest({\n partnerId: partnerId,\n marketId: marketID,\n appKeys: appKeys,\n includeNotEnabled: true,\n projectionFilter: {\n paths: ['sharedMarketingInformation'],\n },\n }),\n );\n }),\n map((res) => {\n // cache whitelabel data in a map for faster access and to prevent redundant API calls\n for (const item of res.apps) {\n if (item?.key?.appId) {\n marketCache.set(item.key.appId, item.sharedMarketingInformation);\n }\n }\n return marketCache;\n }),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n private getMarketCache(marketId: string): Map {\n let marketCache = this.itemWhitelabelMap.get(marketId);\n if (!marketCache) {\n marketCache = new Map();\n this.itemWhitelabelMap.set(marketId, marketCache);\n }\n return marketCache;\n }\n}\n", "{\n \"FRONTEND\": {\n \"STORE\": {\n \"TOTAL\": \"Total\",\n \"SUBTOTAL\": \"Subtotal\",\n \"TAX\": \"Tax ({{percentage}}%)\",\n \"RECURRING_CHARGES_SUMMARY\": {\n \"TITLE\": \"Recurring charges summary\",\n \"NO_RECURRING_CHARGES\": \"No recurring charges\"\n },\n \"CANNOT_ADD_TO_CART_TOOLTIP\": \"This product can't be added to your cart right now. Please contact us directly to purchase this product.\",\n \"CAN_ENABLE_PRODUCTS_PERMISSION_DISABLED_TOOLTIP\": \"Contact an admin to start selling this product or update your user permissions.\",\n \"CONTRACT_DURATION\": \"Contract duration\",\n \"CONTACT_SALES\": \"Contact Sales\",\n \"CONTACT_US\": \"Contact us\",\n \"MESSAGE_US\": \"Message us\",\n \"FREE\": \"Free\",\n \"STARTING_AT\": \"Starting at\",\n \"ESTIMATED_VALUE\": \"Estimated value\",\n \"ENABLED_UPPER\": \"ENABLED\",\n \"ENABLE_UPPER\": \"ENABLE\",\n \"MIN_TO_MAX\": \"{{minRange}} to {{maxRange}}\",\n \"PRODUCT_PRICING_WITH_FREQUENCY\": \"{{pricingTier}} accounts {{frequency}}\",\n \"COMMITMENT_WITH_RENEWAL\": \"*{{initial}} {{frequencyString}} minimum, renews for {{recurring}} {{frequencyString}} periods\",\n \"COMMITMENT\": \"${{initial}} ${{frequencyString}} commitment\",\n \"PRICING_FREQUENCIES\": {\n \"MONTH\": \"month\",\n \"YEAR\": \"year\"\n },\n \"REQUIRES_DESCRIPTOR\": \"(Requires {{requirementName}})\",\n \"ADMINISTRATIVE_QUESTIONS\": \"Administrative Questions\",\n \"AUTO_TITLE_TEXT\": {\n \"SOME_FIELDS_INVALID\": \"Fields require your attention\",\n \"OPTIONAL_FIELDS_UNANSWERED\": \"Optional fields unanswered\",\n \"COMPLETE\": \"Complete\"\n },\n \"REQUIRES\": \"Requires\",\n \"ENABLED_WITH_PREREQUISITE\": \"Enabled with {{prerequisite}}\",\n \"ENABLE_WITH_PREREQUISITE\": \"Enable with {{prerequisite}}\",\n \"PRICE_APPLIES_PER_UNIT\": \"Price applies per unit within a range\",\n \"PRICE_APPLIES_TO_ALL_UNITS\": \"Price applies to all units once range is met\",\n \"PRICING\": \"Pricing\",\n \"PRICE\": \"Price\",\n \"WHOLESALE_COST\": \"Wholesale Cost\",\n \"FAQS\": \"FAQs\",\n \"GALLERY\": \"Gallery\",\n \"FILES\": \"Files\",\n \"GET_IT_NOW\": \"Get It Now\",\n \"CONTAINS_NUM_ITEMS\": \"Contains {{itemCount}} items\",\n \"ENABLED_STATE\": {\n \"ENABLED\": \"Enabled\",\n \"ENABLE\": \"Enable\",\n \"ADD_ON\": {\n \"ENABLE\": \"Enable Add-on\"\n }\n },\n \"STORE_STATE\": {\n \"START_SELLING\": \"Start selling\",\n \"STOP_SELLING\": \"Stop selling\",\n \"SELLING\": \"Selling\",\n \"ADD_ON\": {\n \"START_SELLING\": \"Start selling Add-on\"\n }\n },\n \"MORE\": \"More\",\n \"TAGS\": \"Tags\",\n \"COMMIT\": \"commitment\",\n \"INITIAL\": \"Initial\",\n \"RECURRING\": \"recurring every\",\n \"ACTIVATIONS\": \"Activations\",\n \"ACCOUNTS\": \"accounts\",\n \"ADD_ONS\": \"Add-Ons\",\n \"REQUIRED\": \"Required\",\n \"PERIODS\": {\n \"YEARLY\": \"Yearly\",\n \"MONTHLY\": \"Monthly\",\n \"WEEKLY\": \"Weekly\",\n \"ONE_TIME\": \"One time\",\n \"BIWEEKLY\": \"Bi-weekly\",\n \"DAILY\": \"Daily\"\n },\n \"DURATION\": {\n \"DAY_CONTRACT\": \"The duration of this contract is {{value}} day(s)\",\n \"WEEK_CONTRACT\": \"The duration of this contract is {{value}} week(s)\",\n \"MONTH_CONTRACT\": \"The duration of this contract is {{value}} month(s)\",\n \"YEAR_CONTRACT\": \"The duration of this contract is {{value}} year(s)\"\n },\n \"ORDER_FORM\": {\n \"CONTACT_DESCRIPTION\": \"They may be contacted regarding fulfillment\",\n \"CONTACT_LABEL\": \"Who is the main point of contact?\",\n \"FIELD_TYPE_REQUIRED\": \"Form field type is required\",\n \"FOR_OFFICE_USE_ONLY\": \"For office use only\",\n \"HIDDEN_FROM_END_USERS\": \"Hidden from end users\",\n \"FOR_OFFICE_USE_ONLY_TOOLTIP\": \"This question will not be shown to end users\",\n \"OFFICE_EDITABLE_ONLY\": \"Office editable only\",\n \"OFFICE_EDITABLE_ONLY_TOOLTIP\": \"Answers to this question cannot be changed by end users\",\n \"OPTION\": \"Option\",\n \"VALUE\": \"Value\",\n \"ASSIGN_VALUE\": \"Assign value\",\n \"CLEAR_BUTTON\": \"Clear button\",\n \"ALLOW_MULTIPLES\": \"Allow multiples\",\n \"ALLOW_DUPLICATES\": \"Allow duplicates\",\n \"MAX_NUM_CHOICES\": \"Maximum number of choices\",\n \"CHOICES_HINT\": \"Must be greater than {{minChoices}}\",\n \"CHOICES_ERROR\": \"Max Choices must be a number larger than {{minChoices}}\",\n \"LABEL_HINT\": \"Keep it short. Recommended: Max {{maxCharacters}} characters\",\n \"LABEL_ERROR\": \"Label is required\",\n \"HIDE_PREFIX_SUFFIX\": \"Hide Prefix/Suffix\",\n \"PREFIX_SUFFIX\": \"Prefix/Suffix\",\n \"HIDDEN_NOTE\": \"Values will apply even though currently hidden\",\n \"ID_HINT\": \"ID must be unique\",\n \"ID_ERROR\": \"ID is required\",\n \"UPLOAD_URL\": \"Upload URL\",\n \"UPLOAD_URL_HINT\": \"An upload url will be provided if this field is left blank\",\n \"REGEX_VALIDATOR\": \"Regex validator\",\n \"REGEX_VALIDATOR_ERROR\": \"Must be a valid regular expression\",\n \"REGEX_ERROR_MESSAGE\": \"Regex error message\",\n \"REGEX_ERROR_MESSAGE_HINT\": \"Error displayed to the user when the regular expression is not matched\",\n \"VALIDATION\": \"Validation\",\n \"HIDE_VALIDATION\": \"Hide validation\",\n \"HINT_TEXT\": \"Hint text\",\n \"HINT_TEXT_HINT\": \"Hint text appears below the field. Use it to offer context or examples of the information to provide.\",\n \"DELETE_FIELD\": \"Delete field\",\n \"NO_USERS_INSTRUCTIONS\": \"No Users \u2014 Please add one to this account\",\n \"FIELD_TYPES\": {\n \"FILES\": \"Files\",\n \"DROP_DOWN\": \"Drop Down\",\n \"CHECK_BOX\": \"Check Box\",\n \"TEXT_AREA\": \"Text Area\",\n \"TEXT_BOX\": \"Text Box\",\n \"END_USER\": \"End User\"\n },\n \"LABELS\": {\n \"COMMON_FORM_HEADER_TITLE\": \"Gather activation information\",\n \"COMMON_FORM_HEADER_TEXT\": \"The following information will be provided to the products that require it.\",\n \"COMMON_FORM_EDITING_HINT\": \"Editing this information does not update the account\",\n \"BUSINESS_SECTION_TITLE\": \"Business\",\n \"SALESPERSON_SECTION_TITLE\": \"Salesperson\",\n \"CONTACT_SECTION_TITLE\": \"Contact\",\n \"BUSINESS_NAME\": \"Business Name\",\n \"BUSINESS_ADDRESS\": \"Business Address\",\n \"BUSINESS_PHONE_NUMBER\": \"Business Phone Number\",\n \"BUSINESS_WEBSITE\": \"Business Website\",\n \"BUSINESS_ID\": \"Business ID\",\n \"SALESPERSON_NAME\": \"Salesperson Name\",\n \"SALESPERSON_PHONE_NUMBER\": \"Salesperson Phone Number\",\n \"SALESPERSON_EMAIL\": \"Salesperson Email\",\n \"CONTACT_NAME\": \"Contact Name\",\n \"CONTACT_PHONE_NUMBER\": \"Contact Phone Number\",\n \"CONTACT_EMAIL\": \"Contact Email\"\n },\n \"ACCURATE_INFO_PROMPT\": \"Review to ensure accurate information\",\n \"ACCOUNT_INFO_TITLE\": \"{{ name }}'s information\",\n \"ACCOUNT_DETAILS\": \"Account details\",\n \"EDIT_BUSINESS_DETAILS_DISCLAIMER\": \"Editing this information does not update the business details\",\n \"EDIT_RESELLER_DETAILS_DISCLAIMER\": \"Editing this information does not update the contact details\",\n \"PLEASE_REVIEW\": \"Please review\",\n \"ACCOUNT_CONTACT\": \"Account contact\",\n \"RESELLER_INFO_TITLE\": \"{{ name }}'s primary contact\",\n \"PARTNER_CONTACT\": \"Partner contact\",\n \"ADMINISTRATIVE_INFO\": \"Administrative information\",\n \"ORDER_FORMS\": \"Order forms\",\n \"ORDER_FORMS_SUBTITLE\": \"Additional information is needed for items in this order\",\n \"ACCOUNT\": \"Account\",\n \"RESELLER\": \"Reseller\"\n },\n \"ACTIVATION_STATUS\": {\n \"ACTIVATED\": \"Activated\",\n \"ALREADY_ACTIVATED\": \"Already Activated\",\n \"ACTIVATION_ERRORS\": \"Activation Errors\",\n \"ACTIVATION_ERRORS_OCCURRED\": \"Activation Errors Occured\",\n \"ACTIVATION_WARNINGS\": \"Activation Warnings\"\n },\n \"SEE_ALL\": \"See all\",\n \"NOT_AVAILABLE_IN_YOUR_COUNTRY\": \"Not available in your country\",\n \"AVAILABLE_IN\": \"AVAILABLE IN\",\n \"ALL_COUNTRIES\": \"All countries\",\n \"NEED_HELP\": \"Need help\",\n \"VIDEOS\": \"Videos\",\n \"STOREFRONT_FILTERS\": {\n \"TITLE\": \"Store Filters\",\n \"CATEGORIES\": \"Categories\",\n \"LMI_CATEGORY\": \"LMI Category\"\n },\n \"LMI_CATEGORIES\": {\n \"LISTINGS\": \"Listings\",\n \"SOCIAL\": \"Social\",\n \"REPUTATION\": \"Reputation\",\n \"SEO\": \"SEO\",\n \"WEBSITE\": \"Website\",\n \"ADVERTISING\": \"Advertising\",\n \"CONTENT_AND_EXPERIENCE\": \"Content + Experience\"\n },\n \"PRODUCTS_AND_ADDONS\": \"Related Products & Add-ons\",\n \"ADD_TO_CART\": \"Add to cart\",\n \"BUY_IT_NOW\": \"Buy it now\",\n \"ERROR_PRODUCT\": \"There was a problem loading the product\",\n \"PRODUCT_DETAILS\": {\n \"TAB_LABELS\": {\n \"PRODUCT_INFORMATION\": \"Product Information\",\n \"SCREENSHOTS_AND_FILES\": \"Screenshots & Files\",\n \"EDITIONS_AND_PRICING\": \"Editions & Pricing\"\n },\n \"PRODUCT_REQUIREMENTS\": {\n \"LABEL\": \"REQUIRED PRODUCTS\",\n \"TOOLTIP\": {\n \"TITLE\": \"Required products\",\n \"INTERNAL_DESCRIPTION\": \"Required products need to be already active on a customer's account before they can buy or use this product.\",\n \"EXTERNAL_DESCRIPTION\": \"You\u2019ll need these required products before you can buy or use this product.\"\n }\n },\n \"PREVIEW_FORMS\": {\n \"LABEL\": \"REQUIRED FORMS\",\n \"ORDER_FORM\": \"Order form\",\n \"FULFILLMENT_FORM\": \"Fulfillment form\",\n \"ORDER_FORM_PREVIEW\": \"Order form preview\",\n \"ORDER_FORM_PREVIEW_ALERT\": \"This is a preview of the order form used when purchasing this product. This preview is for display purposes only. No input in this preview will be saved.\",\n \"TOOLTIP\": {\n \"TITLE\": \"Required forms\",\n \"DESCRIPTION\": \"Order form information is required at the time of purchase. Fulfillment form information is required after purchase.\"\n }\n }\n },\n \"EDITION_SELECTOR\": {\n \"ADD_APP_SELECTION\": {\n \"TITLE\": \"Choose an edition\"\n },\n \"ADD_DEPENDANT_SELECTION\": {\n \"TITLE\": \"Choose an edition of {{productName}}\",\n \"EXPLANATION\": \"{{childName}} is a dependent of {{productName}}, which you haven\u2019t already purchased. Choose an edition of {{productName}} to add both to your cart.\"\n },\n \"ADD_ADDON_SELECTION\": {\n \"TITLE\": \"Choose an edition of {{productName}}\",\n \"EXPLANATION\": \"{{childName}} is an add-on to {{productName}}, which you haven\u2019t already purchased. Choose an edition of {{productName}} to add both to your cart.\"\n },\n \"COMPARE_BUTTON_LABEL\": \"Compare editions\"\n },\n \"ORDER_SUMMARY\": {\n \"REQUIRES\": \"Requires {{parentName}}\",\n \"ADDON_OF\": \"Add-on of {{parentName}}\"\n },\n \"MANAGE_PRODUCT\": {\n \"MANAGE_PRODUCT_TABLE\": {\n \"SHOW_GROWTH_OVER\": \"* Show growth over\",\n \"PARENT_REQUIREMENT\": \"Requires {{name}}\",\n \"TOOLBAR_TEXT\": \"{{numShowing}} of {{numTotal}} products\",\n \"COLUMNS\": {\n \"PRODUCT\": \"Product\",\n \"ACTIVE_ACCOUNTS\": \"Active accounts\",\n \"ACTIVATIONS\": \"Activations*\",\n \"PENDING_DEACTIVATIONS\": \"Pending deactivations*\",\n \"DEACTIVATIONS\": \"Deactivations*\",\n \"GROWTH\": \"Growth*\",\n \"SELLING_IN_STORE\": \"Selling in store\"\n },\n \"ROW_ACTIONS\": {\n \"VIEW_MARKETING_MATERIAL\": \"View marketing material\",\n \"EDIT_IN_MARKETPLACE\": \"Edit in Marketplace\"\n }\n }\n }\n }\n }\n}\n", "export const WEBLATE_COMPONENT_NAME = 'common/store';\n", "import { Component, Input, OnChanges, SimpleChanges, Inject } from '@angular/core';\nimport { BilledProduct, PricingTier } from '@vendasta/shared';\nimport { MarketplaceBillingFrequency, DisplayPriceService } from '@vendasta/shared';\nimport { Pricing, Price } from './pricing';\nimport { BehaviorSubject, combineLatest, Observable, of, EMPTY } from 'rxjs';\nimport { TranslateService } from '@ngx-translate/core';\nimport { map, switchMap, expand, reduce, catchError, publishReplay, refCount, filter } from 'rxjs/operators';\nimport { Discount, ListDiscountsRequestFiltersInterface, DiscountService } from '@galaxy/billing';\nimport { HttpErrorResponse } from '@angular/common/http';\n\n@Component({\n selector: 'store-base-pricing',\n template: '',\n})\nexport class VaBasePricingComponent implements OnChanges {\n @Input() pricing: Pricing = null;\n @Input() billedProduct: BilledProduct;\n @Input() wrapFrequency = false;\n @Input() isAddon = false;\n @Input() hasVerifiedContract = false;\n @Input() highlightPrice = true;\n @Input() loaded = true;\n @Input() showAllPrices = false; // When false this will only show the first element in the pricing.prices list\n @Input() useAbbreviatedFrequency = false;\n @Input() showIsStartingAt = false;\n\n pricing$$ = new BehaviorSubject(null);\n billedProduct$$ = new BehaviorSubject(null);\n hasVerifiedContract$$ = new BehaviorSubject(false);\n\n isFree$: Observable;\n shouldContactSales$: Observable;\n isFlatPrice$: Observable;\n\n pricingTierData$: Observable<\n {\n isStartingPrice: boolean;\n pricingTierString: string;\n pricingTierForProduct: string;\n pricingTierForAddon: string;\n priceStringForTier: string;\n priceStringForTierWithoutFreq: string;\n }[]\n >;\n\n priceData$: Observable<\n {\n isStartingPrice: boolean;\n priceStringForPricingWithoutFrequency: string;\n priceStringForPricingWithFrequency: string;\n frequencyString: string;\n abbreviatedFrequencyString: string;\n }[]\n >;\n\n commitmentMessage$: Observable;\n setupFeeString$: Observable;\n isFirstTierFree$: Observable;\n showPricingPopover = false;\n discounts$: Observable;\n\n get pricing$(): Observable {\n return this.pricing$$.asObservable();\n }\n\n get billedProduct$(): Observable {\n return this.billedProduct$$.asObservable();\n }\n\n get hasVerifiedContract$(): Observable {\n return this.hasVerifiedContract$$.asObservable();\n }\n\n constructor(\n private translateService: TranslateService,\n private displayPriceService: DisplayPriceService,\n @Inject('PARTNER_ID') readonly partnerId$: Observable,\n private discountService: DiscountService,\n ) {\n this.isFree$ = combineLatest([this.pricing$, this.billedProduct$, this.hasVerifiedContract$]).pipe(\n map(([pricing, billedProduct, hasVerifiedContract]) => {\n const isFreeHelper = (prices: { price: number }[]): boolean => {\n return prices ? prices.every((p) => p.price === 0 || p.price === undefined) : false;\n };\n\n if (hasVerifiedContract && billedProduct) {\n return billedProduct.pricingTiers.length === 1 ? isFreeHelper(billedProduct.pricingTiers) : false;\n }\n return !!pricing && isFreeHelper(pricing.prices);\n }),\n );\n\n this.shouldContactSales$ = combineLatest([this.pricing$, this.billedProduct$, this.hasVerifiedContract$]).pipe(\n map(([pricing, billedProduct, hasVerifiedContract]) => {\n const nestedPricesIsContactSales = (prices: { price: number }[]) => {\n return prices && prices.length > 0 ? prices.some((p) => p.price === null || p.price < 0) : true;\n };\n\n if (hasVerifiedContract && billedProduct) {\n return nestedPricesIsContactSales(billedProduct.pricingTiers);\n }\n if (!pricing) {\n return true;\n }\n return nestedPricesIsContactSales(pricing.prices);\n }),\n );\n\n this.isFlatPrice$ = this.billedProduct$.pipe(map((billedProduct) => billedProduct.pricingTiers.length === 1));\n this.pricingTierData$ = this.billedProduct$.pipe(\n switchMap((billedProduct) => {\n const pricingTierObservables = billedProduct.pricingTiers.map((pricingTier) => {\n return combineLatest([\n of(pricingTier.isStartingPrice),\n this.buildPricingTierString(pricingTier),\n this.buildPricingTierForProduct(pricingTier, billedProduct.billingFrequency),\n this.buildPricingTierForAddon(pricingTier),\n this.buildPriceStringForTier(pricingTier, billedProduct.billingFrequency, pricingTier.isStartingPrice),\n this.buildPriceStringForTier(pricingTier),\n ]).pipe(\n map(\n ([\n isStartingPrice,\n pricingTierString,\n pricingTierForProduct,\n pricingTierForAddon,\n priceStringForTier,\n priceStringForTierWithoutFreq,\n ]) => {\n return {\n isStartingPrice: isStartingPrice,\n pricingTierString: pricingTierString,\n pricingTierForProduct: pricingTierForProduct,\n pricingTierForAddon: pricingTierForAddon,\n priceStringForTier: priceStringForTier,\n priceStringForTierWithoutFreq: priceStringForTierWithoutFreq,\n };\n },\n ),\n );\n });\n return combineLatest([...pricingTierObservables]);\n }),\n );\n\n this.priceData$ = this.pricing$.pipe(\n switchMap((pricing) => {\n let filteredPrices = pricing.prices;\n if (pricing.prices.length > 1) {\n filteredPrices = pricing.prices.filter((price) => price.price > 0).sort(this.sortPricesByFrequency);\n }\n const priceObservables = filteredPrices.map((price) => {\n return combineLatest([\n of(price.isStartingPrice),\n this.buildPriceStringForPricing(price, pricing.currency, true, false),\n this.buildPriceStringForPricing(price, pricing.currency, false, true),\n this.buildFrequencyString(price.frequency),\n of(this.buildAbbreviatedFrequencyString(price.frequency)),\n ]).pipe(\n map(\n ([\n isStartingPrice,\n priceStringForPricingWithoutFrequency,\n priceStringForPricingWithFrequency,\n frequencyString,\n abbrevFreq,\n ]) => {\n return {\n isStartingPrice: isStartingPrice,\n priceStringForPricingWithoutFrequency: priceStringForPricingWithoutFrequency,\n priceStringForPricingWithFrequency: priceStringForPricingWithFrequency,\n frequencyString: frequencyString,\n abbreviatedFrequencyString: abbrevFreq,\n };\n },\n ),\n );\n });\n return combineLatest([...priceObservables]);\n }),\n );\n\n this.commitmentMessage$ = this.buildCommitmentMessage();\n this.setupFeeString$ = this.displayPriceService.getSetupFeeString();\n this.isFirstTierFree$ = this.billedProduct$.pipe(\n map((billedProduct) => {\n return billedProduct.pricingTiers.length > 1 && billedProduct.pricingTiers.some((item) => item.price === 0);\n }),\n );\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (Object.prototype.hasOwnProperty.call(changes, 'pricing')) {\n this.pricing$$.next(changes.pricing.currentValue);\n }\n if (Object.prototype.hasOwnProperty.call(changes, 'billedProduct')) {\n this.billedProduct$$.next(changes.billedProduct.currentValue);\n }\n if (Object.prototype.hasOwnProperty.call(changes, 'hasVerifiedContract')) {\n this.hasVerifiedContract$$.next(changes.hasVerifiedContract.currentValue);\n }\n }\n\n buildPricingTierString(tier: PricingTier): Observable {\n let max = String(tier.rangeMax);\n if (tier.rangeMax === -1) {\n max = '∞';\n if (tier.rangeMin === 0 || tier.rangeMin === 1) {\n return of('');\n }\n }\n return this.translateService.stream('FRONTEND.STORE.MIN_TO_MAX', {\n minRange: String(tier.rangeMin),\n maxRange: max,\n });\n }\n\n buildPricingTierForProduct(tier: PricingTier, frequency?: string): Observable {\n return combineLatest([this.buildPricingTierString(tier), this.buildFrequencyString(frequency)]).pipe(\n switchMap(([pricingTier, formattedFrequency]) => {\n if (pricingTier) {\n return this.translateService.stream('FRONTEND.STORE.PRODUCT_PRICING_WITH_FREQUENCY', {\n pricingTier: pricingTier,\n frequency: formattedFrequency,\n });\n }\n return of(formattedFrequency);\n }),\n );\n }\n\n buildPricingTierForAddon(tier: PricingTier): Observable {\n return this.buildPricingTierString(tier).pipe(map((pricingRange) => (pricingRange ? pricingRange : '')));\n }\n\n sortPricesByFrequency(priceA: Price, priceB: Price): number {\n // this map is coded based on frequency values described by the enums described in the marketplace packages vendasta\n // apis and the corresponding translations to strings laced throughout galaxy\n const priceFrequencyComparisonMap: Map> = new Map>([\n [\n 'MONTHLY',\n new Map([\n ['MONTHLY', 0],\n ['YEARLY', -1],\n ['ONCE', -1],\n ['ONE TIME', -1],\n ['ONE_TIME', -1],\n ]),\n ],\n [\n 'YEARLY',\n new Map([\n ['MONTHLY', 1],\n ['YEARLY', 0],\n ['ONCE', -1],\n ['ONE TIME', -1],\n ['ONE_TIME', -1],\n ]),\n ],\n [\n 'ONCE',\n new Map([\n ['MONTHLY', 1],\n ['YEARLY', 1],\n ['ONCE', 0],\n ['ONE TIME', 0],\n ['ONE_TIME', 0],\n ]),\n ],\n [\n 'ONE TIME',\n new Map([\n ['MONTHLY', 1],\n ['YEARLY', 1],\n ['ONCE', 0],\n ['ONE TIME', 0],\n ['ONE_TIME', 0],\n ]),\n ],\n [\n 'ONE_TIME',\n new Map([\n ['MONTHLY', 1],\n ['YEARLY', 1],\n ['ONCE', 0],\n ['ONE TIME', 0],\n ['ONE_TIME', 0],\n ]),\n ],\n ]);\n\n if (priceFrequencyComparisonMap.get(priceA.frequency.toUpperCase()) === undefined) {\n return 0;\n }\n\n const compareVal = priceFrequencyComparisonMap\n .get(priceA.frequency.toUpperCase())\n .get(priceB.frequency.toUpperCase());\n return compareVal ? compareVal : 0;\n }\n\n buildPriceStringForPricing(\n price: Price,\n currency: string,\n excludeFrequency: boolean,\n canIncludeStartingAtPrefix: boolean,\n ): Observable {\n return this.displayPriceService.formatDisplayPrice(\n price.price,\n currency,\n (excludeFrequency ? '' : price.frequency) as any,\n true,\n true,\n true,\n price.isStartingPrice && canIncludeStartingAtPrefix,\n );\n }\n\n buildPriceStringForTier(\n tier: PricingTier,\n frequency?: MarketplaceBillingFrequency,\n isStartingPrice?: boolean,\n ): Observable {\n return this.displayPriceService.formatDisplayPrice(\n tier.price,\n this.billedProduct.currency,\n frequency as any,\n undefined,\n undefined,\n undefined,\n isStartingPrice,\n );\n }\n\n buildFrequencyString(frequency?: string): Observable {\n return this.displayPriceService.formatBillingFrequency(frequency);\n }\n\n buildAbbreviatedFrequencyString(frequency?: string): string {\n switch ((frequency || '').toLowerCase()) {\n case 'daily':\n return 'dy';\n case 'weekly':\n return 'wk';\n case 'monthly':\n return 'mo';\n case 'yearly':\n return 'yr';\n }\n return null;\n }\n\n buildCommitmentMessage(): Observable {\n return combineLatest([\n this.translateService.stream('FRONTEND.STORE.PRICING_FREQUENCIES'),\n this.billedProduct$,\n this.pricing$,\n this.isFree$,\n this.shouldContactSales$,\n ]).pipe(\n switchMap(([frequencyTranslations, billedProduct, pricing, isFree, shouldContactSales]) => {\n const frequency = billedProduct\n ? billedProduct.billingFrequency.toString().toLowerCase()\n : pricing && pricing.prices.length > 0\n ? pricing.prices[0].frequency.toLowerCase()\n : '';\n let frequencyString = '';\n if (frequency === 'yearly') {\n frequencyString = frequencyTranslations.YEAR;\n } else if (frequency === 'monthly') {\n frequencyString = frequencyTranslations.MONTH;\n }\n if (!frequencyString || isFree || shouldContactSales) {\n return of('');\n }\n if (billedProduct && billedProduct.commitment) {\n const initial = billedProduct.commitment.initial;\n const recurring = billedProduct.commitment.recurring;\n return this.translateService.stream('FRONTEND.STORE.COMMITMENT_WITH_RENEWAL', {\n initial: initial,\n frequencyString: frequencyString,\n recurring: recurring,\n });\n }\n return of('');\n }),\n );\n }\n\n private fetchAllDiscounts(pid: string, sku: string): Observable {\n const pageSize = 50;\n const filters: ListDiscountsRequestFiltersInterface = {\n merchantId: pid,\n expiry: new Date(),\n skus: [sku],\n };\n\n return this.discountService.list(filters, '', pageSize).pipe(\n expand((resp) => {\n if ((resp?.results || []).length >= pageSize) {\n return this.discountService.list(filters, resp.nextCursor, pageSize);\n } else {\n return EMPTY;\n }\n }),\n reduce((prev, curr) => {\n const discounts = curr?.results || [];\n return prev.concat(discounts);\n }, [] as Discount[]),\n );\n }\n\n private getDiscounts(): Observable {\n return combineLatest([this.partnerId$, this.billedProduct$]).pipe(\n filter(([pid, billedProduct]) => !!pid && !!billedProduct),\n switchMap(([pid, billedProduct]) => {\n return this.fetchAllDiscounts(pid, billedProduct.productId);\n }),\n catchError((err: HttpErrorResponse) => {\n console.error('error getting discount information:', err);\n return of([]);\n }),\n publishReplay(1),\n refCount(),\n );\n }\n\n itemInfoIconEnter(): void {\n if (this.discounts$ === undefined) {\n this.discounts$ = this.getDiscounts();\n }\n\n this.showPricingPopover = true;\n }\n\n itemInfoIconLeave(): void {\n this.showPricingPopover = false;\n }\n}\n", "import { Component } from '@angular/core';\nimport { VaBasePricingComponent } from '../base-pricing.component';\n\n@Component({\n selector: 'store-highlight-pricing',\n templateUrl: './highlight-pricing.component.html',\n styleUrls: ['./highlight-pricing.component.scss'],\n})\nexport class HighlightPricingComponent extends VaBasePricingComponent {}\n", "\n \n {{ 'FRONTEND.STORE.CONTACT_SALES' | translate }}\n \n \n
\n {{ 'FRONTEND.STORE.FREE' | translate }}\n
\n \n \n \n
\n \n \n {{ 'FRONTEND.STORE.STARTING_AT' | translate }}\n \n \n {{ tier.priceStringForTierWithoutFreq }}\n \n \n \n {{ tier.pricingTierForProduct }}\n \n
\n
\n \n \n \n \n \n {{ 'FRONTEND.STORE.STARTING_AT' | translate }}\n \n \n 0\">+\n {{ price.priceStringForPricingWithoutFrequency }}\n \n \n /{{ price.abbreviatedFrequencyString }}\n \n \n \n {{ price.frequencyString }}\n \n \n \n \n
\n \n
\n
\n \n {{ tier.pricingTierForAddon }}\n \n \n \n {{ 'FRONTEND.STORE.STARTING_AT' | translate }}\n \n \n {{ tier.priceStringForTier }}\n \n \n
\n
\n \n
\n \n {{ (priceData$ | async)[0]?.frequencyString }}\n \n \n \n {{ 'FRONTEND.STORE.STARTING_AT' | translate }}\n \n \n {{\n (priceData$ | async)[0]?.priceStringForPricingWithoutFrequency\n }}\n \n \n
\n
\n
\n 0\"\n >\n + {{ billedProduct.setupFee | price$: billedProduct.currency | async }}\n {{ setupFeeString$ | async }}\n \n 1\n \"\n >\n {{ commitmentMessage$ | async }}\n \n \n \n\n\n
\n
\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport moment from 'moment';\n\nexport enum formatting {\n datetime = 'Y-MM-DD h:mm:ss A', //2018-01-25 11:54:39 AM\n date = 'Y-MM-DD', //2018-01-25\n month = 'MMMM Y', //January 2018\n utc = 'YYYY-MM-DD[T00:00:00Z]', //2018-01-25T00:00:00Z\n longDate = 'dddd MMMM Do, YYYY', //Thursday January 25th, 2018\n shortSpoken = 'MMM D, Y', //Jan 25, 2018\n}\n\nexport function formatDate(\n date: Date | string,\n format: formatting,\n translateEmpty?: string,\n useLocalTime?: boolean,\n): string {\n translateEmpty = translateEmpty ? translateEmpty : '';\n if (!date) {\n return translateEmpty;\n }\n\n if (typeof date === 'string') {\n date = new Date(date);\n if (date.toString() === 'Invalid Date') {\n return date.toString();\n }\n }\n\n let m = moment(date.toISOString());\n if (format === formatting.utc || !useLocalTime) {\n m = moment.utc(date.toISOString());\n }\n\n return m.format(format);\n}\n\n@Pipe({ name: 'formatDate' })\nexport class FormatDatePipe implements PipeTransform {\n transform(date: Date | string, format: formatting, translateEmpty?: string, useLocalTime?: boolean): string {\n return formatDate(date, format, translateEmpty, useLocalTime);\n }\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\nimport { Discount, DiscountType, Currency } from '@galaxy/billing';\nimport { formatDate, formatting } from './format-date.pipe';\nimport { formatDisplayPrice } from '@vendasta/shared';\n\n/*\n * Format a discount for display.\n *\n * Usage:\n * discount | formatDiscount\n * Examples:\n * {{ { amount: 100, discountType: 'FIXED_AMOUNT', start: '2019-12-12' } | formatDiscount: 'USD' }}\n * formats to: $1.00 off starting 2019-12-12\n *\n * {{ { amount: 15, discountType: 'PERCENT', start: '2019-08-01' end: '2019-09-01' } | formatDiscount: 'USD' }}\n * formats to: 15% off starting 2019-08-01 to 2019-09-01\n */\n@Pipe({ standalone: true, name: 'formatDiscount' })\nexport class FormatDiscountPipe implements PipeTransform {\n transform(discount: Discount, currency: Currency): string {\n if (!discount) {\n return 'N/A';\n }\n\n const amount = formatDiscountAmount(discount.amount, discount.discountType, currency);\n\n let off = 'off';\n switch (discount.discountType) {\n case DiscountType.FIXED_NUMBER_OF_UNITS: {\n off = 'free units';\n if (discount.resetEachPeriod) {\n off += ' per period';\n }\n break;\n }\n case DiscountType.FIXED_AMOUNT_PER_UNIT: {\n off = 'off each unit';\n break;\n }\n }\n\n const start = formatDate(discount.start, formatting.date, '', true);\n const dateRange =\n discount.end.valueOf() !== new Date('0001-01-01T00:00:00Z').valueOf()\n ? `${start} to ${formatDate(discount.end, formatting.date, '', true)}`\n : start;\n\n return `${amount} ${off} starting ${dateRange}`;\n }\n}\n\nfunction formatDiscountAmount(amount: number, rule: DiscountType, currency: Currency): string {\n if ([DiscountType.FIXED_AMOUNT, DiscountType.FIXED_AMOUNT_PER_UNIT].includes(rule)) {\n return formatDisplayPrice(amount, currency, null, true, false, true, false);\n } else if (rule === DiscountType.PERCENT_AMOUNT) {\n return amount.toString() + '%';\n }\n return amount.toString();\n}\n", "import { Component } from '@angular/core';\nimport { VaBasePricingComponent } from '../base-pricing.component';\n\n@Component({\n selector: 'store-table-pricing',\n templateUrl: './table-pricing.component.html',\n styleUrls: ['./table-pricing.component.scss'],\n})\nexport class TablePricingComponent extends VaBasePricingComponent {}\n", "\n \n {{ 'FRONTEND.STORE.STARTING_AT' | translate }}\n \n
\n \n
\n {{ (pricingTierData$ | async)[0]?.priceStringForTier }}\n
\n
\n \n \n
Starting at
\n {{ 'FRONTEND.STORE.FREE' | translate }}\n \n info_outline\n \n\n \n
\n
\n {{\n 'FRONTEND.TABLE_PRICING.WHOLESALE_PRICING_INFORMATION'\n | translate\n }}\n
\n\n
\n \n
\n {{ tier.pricingTierString }}\n
\n
\n {{ tier.priceStringForTier }}\n
\n
\n
0\">\n
\n
\n +\n {{\n billedProduct.setupFee\n | price$: billedProduct.currency\n | async\n }}\n {{ setupFeeString$ | async }}\n
\n
\n
\n\n
\n
\n \n {{ 'FRONTEND.TABLE_PRICING.ACTIVE_DISCOUNTS' | translate }}\n \n
\n
    \n
  • \n {{ disc | formatDiscount: billedProduct?.currency }}\n
  • \n
\n
\n \n \n
\n\n \n
\n \n
{{ tier.pricingTierString }}
\n
\n {{ tier.priceStringForTier }}\n
\n
\n
0\">\n
\n
\n +\n {{\n billedProduct.setupFee | price$: billedProduct.currency | async\n }}\n {{ setupFeeString$ | async }}\n
\n
\n \n
\n
\n 0 && isFlatPrice$ | async\"\n >\n + {{ billedProduct.setupFee | price$: billedProduct.currency | async }}\n {{ setupFeeString$ | async }}\n \n 1\n \"\n >\n {{ commitmentMessage$ | async }}\n \n \n \n
\n \n {{ 'FRONTEND.STORE.CONTACT_SALES' | translate }}\n \n \n \n {{ 'FRONTEND.STORE.FREE' | translate }}\n \n \n \n \n \n 1 }\"\n >\n {{ priceData.priceStringForPricingWithFrequency }}\n \n \n + {{ priceData.priceStringForPricingWithFrequency }}\n \n \n \n \n {{ priceDatas[0].priceStringForPricingWithFrequency }}\n \n \n
0\">\n +\n {{\n billedProduct.setupFee | price$: billedProduct.currency | async\n }}\n {{ setupFeeString$ | async }}\n
\n 1\n \"\n >\n {{ commitmentMessage$ | async }}\n
\n
\n \n \n \n
\n\n
\n
\n", "import { Component } from '@angular/core';\nimport { VaBasePricingComponent } from './base-pricing.component';\n\n@Component({\n selector: 'store-pricing',\n templateUrl: './pricing.component.html',\n styleUrls: ['./pricing.component.scss'],\n})\nexport class VaPricingComponent extends VaBasePricingComponent {}\n", "\n \n\n\n \n\n", "import { Currency, RevenuePeriod } from '@vendasta/sales-orders';\nimport { BillingFrequency } from '@vendasta/shared';\n\nexport function convertSalesOrderCurrencyToString(currency: Currency): string {\n return Currency[currency] ? Currency[currency] : Currency[Currency.USD];\n}\n\nexport function getPeriodTranslationKey(period: RevenuePeriod): string {\n switch (period || RevenuePeriod.ONETIME) {\n case RevenuePeriod.ONETIME:\n return 'FRONTEND.STORE.PERIODS.ONE_TIME';\n case RevenuePeriod.DAILY:\n return 'FRONTEND.STORE.PERIODS.DAILY';\n case RevenuePeriod.WEEKLY:\n return 'FRONTEND.STORE.PERIODS.WEEKLY';\n case RevenuePeriod.BIWEEKLY:\n return 'FRONTEND.STORE.PERIODS.BIWEEKLY';\n case RevenuePeriod.MONTHLY:\n return 'FRONTEND.STORE.PERIODS.MONTHLY';\n case RevenuePeriod.YEARLY:\n return 'FRONTEND.STORE.PERIODS.YEARLY';\n default:\n return '';\n }\n}\n\nexport function salesOrderPeriodToBillingFrequency(period: RevenuePeriod): BillingFrequency {\n switch (period || RevenuePeriod.ONETIME) {\n case RevenuePeriod.ONETIME:\n return 'once';\n case RevenuePeriod.DAILY:\n return 'daily';\n case RevenuePeriod.WEEKLY:\n return 'weekly';\n case RevenuePeriod.BIWEEKLY:\n return 'other';\n case RevenuePeriod.MONTHLY:\n return 'monthly';\n case RevenuePeriod.YEARLY:\n return 'yearly';\n default:\n return 'other';\n }\n}\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatListModule } from '@angular/material/list';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { CoreSharedModule } from '@vendasta/shared';\nimport { GalaxyFrequencyModule } from '@vendasta/galaxy/frequency';\nimport { GalaxyPipesModule } from '@vendasta/galaxy/pipes';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport baseTranslation from '../assets/i18n/en_devel.json';\nimport { WEBLATE_COMPONENT_NAME } from '../shared/constants';\nimport { HighlightPricingComponent } from './highlight-pricing/highlight-pricing.component';\nimport { FormatBillingFrequencyPipe, PricePipe } from './pricing-pipes';\nimport { VaPricingComponent } from './pricing.component';\nimport { SimplePriceDisplayComponent } from './simple-price-display.component';\nimport { StorePriceComponent } from './store-price/store-price.component';\nimport { TablePricingComponent } from './table-pricing/table-pricing.component';\nimport { MatIconModule } from '@angular/material/icon';\nimport { GalaxyPopoverModule } from '@vendasta/galaxy/popover';\nimport { FormatDiscountPipe } from '../shared/format-discount.pipe';\n\n@NgModule({\n imports: [\n CommonModule,\n CoreSharedModule,\n TranslateModule,\n LexiconModule.forChild({\n componentName: WEBLATE_COMPONENT_NAME,\n baseTranslation: baseTranslation,\n }),\n MatListModule,\n GalaxyPipesModule,\n MatIconModule,\n GalaxyPopoverModule,\n GalaxyFrequencyModule,\n FormatDiscountPipe,\n ],\n declarations: [\n VaPricingComponent,\n HighlightPricingComponent,\n TablePricingComponent,\n StorePriceComponent,\n SimplePriceDisplayComponent,\n PricePipe,\n FormatBillingFrequencyPipe,\n ],\n exports: [\n VaPricingComponent,\n SimplePriceDisplayComponent,\n PricePipe,\n FormatBillingFrequencyPipe,\n StorePriceComponent,\n ],\n})\nexport class VaPricingModule {}\n", "import { Component, EventEmitter, Input, Output } from '@angular/core';\n\nimport { ImageTransformationService } from '@vendasta/image-transformation';\nimport { StoreItem } from './store-item';\n\n@Component({\n selector: 'store-card',\n template: `\n \n
\n \n \n
\n
\n \n \n {{ item.name }}\n \n \n \n \n \n \n {{ item.tagline }}\n
\n \n add\n check\n \n {{ (item.purchased ? 'FRONTEND.STORE.ENABLED_UPPER' : 'FRONTEND.STORE.ENABLE_UPPER') | translate }}\n \n \n \n
\n `,\n styleUrls: ['./store-card.component.scss'],\n})\nexport class StoreCardComponent {\n @Input() item: StoreItem;\n @Input() currencyCode: string;\n @Input() showAllPrices = false; // TODO: get rid of this\n @Output() purchasedClicked = new EventEmitter();\n @Output() cardClicked = new EventEmitter();\n @Output() descriptionClicked = new EventEmitter();\n @Output() bannerClicked = new EventEmitter();\n\n constructor(private imageTransformationService: ImageTransformationService) {}\n\n getBannerColorForName(): string {\n // determine an icon color for a product with no icon by using the product name\n const COLOR_CODES = [\n '#EF5350',\n '#42A5F5',\n '#66BB6A',\n '#FFA726',\n '#AB47BC',\n '#FFCA28',\n '#EC407A',\n '#26C6DA',\n '#FF7B57',\n ];\n let nameSum = 0;\n const defaultColor = '#808080';\n if (!this.item.name) {\n return defaultColor;\n }\n for (let i = 0; i < this.item.name.length; i++) {\n nameSum += this.item.name[i].charCodeAt(0);\n }\n const index = nameSum % COLOR_CODES.length;\n return COLOR_CODES[index];\n }\n\n getSrcsetHeaderUrls(imageUrl: string): string {\n return this.imageTransformationService.getSrcSetForImage(imageUrl, [680, 1360, 2720]);\n }\n}\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCardModule } from '@angular/material/card';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { ImageTransformationModule } from '@vendasta/image-transformation';\nimport { VaIconModule } from '@vendasta/uikit';\nimport { VaPricingModule } from './pricing/pricing.module';\nimport { StoreCardComponent } from './store-card.component';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport baseTranslation from './assets/i18n/en_devel.json';\nimport { WEBLATE_COMPONENT_NAME } from './shared/constants';\nimport { GalaxyTooltipModule } from '@vendasta/galaxy/tooltip';\nimport { BillingUiModule } from '@vendasta/billing-ui';\n\n@NgModule({\n imports: [\n CommonModule,\n VaIconModule,\n MatCardModule,\n MatButtonModule,\n MatIconModule,\n MatMenuModule,\n ImageTransformationModule,\n VaPricingModule,\n TranslateModule,\n GalaxyTooltipModule,\n LexiconModule.forChild({\n componentName: WEBLATE_COMPONENT_NAME,\n baseTranslation: baseTranslation,\n }),\n BillingUiModule,\n ],\n declarations: [StoreCardComponent],\n exports: [StoreCardComponent],\n})\nexport class StoreCardModule {}\n", "import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { AppPrice } from '@galaxy/marketplace-apps';\n\n@Component({\n selector: 'store-header-container',\n templateUrl: './header-container.component.html',\n styleUrls: ['./header-container.component.scss'],\n})\nexport class VaHeaderContainerComponent {\n @Input() iconUrl: string;\n @Input() title: string;\n @Input() tagline: string;\n @Input() prerequisite: string;\n @Input() chipLabels: string[];\n @Input() appPrice: AppPrice;\n @Input() partnerCurrency = 'USD';\n @Input() loaded = true;\n @Input() hasVerifiedContract: boolean;\n @Input() primaryPricingActionLabel: string;\n @Input() pricingActionLabel: string;\n @Input() pricingActionEnabled: boolean;\n @Input() actionEnabled: boolean;\n @Input() actionLabel: string;\n @Input() showAction: boolean;\n @Input() showPricing: boolean;\n @Input() showEnableAddon = false;\n @Input() prerequisiteLabel: string;\n @Input() hideTags = false;\n @Output() prerequisiteSelected = new EventEmitter();\n @Output() actionSelected = new EventEmitter();\n @Output() primaryPricingActionSelected = new EventEmitter();\n @Output() pricingActionSelected = new EventEmitter();\n @Output() contactActionClick = new EventEmitter();\n\n onActionSelected(): void {\n this.actionSelected.emit();\n }\n\n onPrerequisiteSelected(): void {\n this.prerequisiteSelected.emit();\n }\n\n handleContactActionClick(): void {\n this.contactActionClick.emit();\n }\n}\n", "
\n
\n
\n \n\n
\n \n {{ title }}\n \n\n \n {{ tagline }}\n \n\n
\n \n {{ 'FRONTEND.STORE.REQUIRES' | translate }}\n \n {{ prerequisite }}\n
\n\n \n \n {{ chip | translate }}\n \n \n
\n
\n\n
\n \n {{ actionLabel }}\n \n\n \n\n \n {{ actionLabel }}\n \n\n
\n {{ 'FRONTEND.STORE.ENABLED_WITH_PREREQUISITE' | translate : { prerequisite: prerequisiteLabel } }}\n
\n\n
\n \n
\n\n

\n \n {{ primaryPricingActionLabel }}\n \n {{ pricingActionLabel }}\n

\n
\n
\n
\n", "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCardModule } from '@angular/material/card';\nimport { MatChipsModule } from '@angular/material/chips';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { BillingUiModule } from '@vendasta/billing-ui';\nimport { VaIconModule } from '@vendasta/uikit';\nimport baseTranslation from '../assets/i18n/en_devel.json';\nimport { VaPricingModule } from '../pricing/pricing.module';\nimport { WEBLATE_COMPONENT_NAME } from '../shared/constants';\nimport { VaHeaderContainerComponent } from './header-container.component';\n\n@NgModule({\n imports: [\n CommonModule,\n MatIconModule,\n VaPricingModule,\n VaIconModule,\n MatChipsModule,\n MatButtonModule,\n MatTooltipModule,\n MatCardModule,\n TranslateModule,\n LexiconModule.forChild({\n componentName: WEBLATE_COMPONENT_NAME,\n baseTranslation: baseTranslation,\n }),\n BillingUiModule,\n ],\n declarations: [VaHeaderContainerComponent],\n exports: [VaHeaderContainerComponent],\n})\nexport class VaHeaderContainerModule {}\n", "import { Injectable } from '@angular/core';\n\ninterface StorageCache {\n [key: string]: any;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class TemporaryStorageService {\n private readonly cache: StorageCache;\n private readonly isSessionStorageAvailable: boolean;\n\n constructor() {\n this.cache = Object.create(null);\n this.isSessionStorageAvailable = !!sessionStorage;\n }\n\n set(key: string, objectToCache: any): void {\n try {\n const serializedCache = JSON.stringify(objectToCache);\n if (this.isSessionStorageAvailable) {\n sessionStorage.setItem(key, serializedCache);\n } else {\n this.cache[key] = serializedCache;\n }\n } catch (error) {\n console.warn('Unable to write to SessionStorage API.');\n console.error(error);\n }\n }\n\n get(key: string): any {\n try {\n let serializedCache = null;\n if (this.isSessionStorageAvailable) {\n serializedCache = sessionStorage.getItem(key);\n } else {\n serializedCache = this.cache[key];\n }\n\n if (serializedCache) {\n return JSON.parse(serializedCache);\n }\n } catch (error) {\n console.warn('Unable to read from SessionStorage API.');\n console.error(error);\n }\n }\n}\n", "import { Input, Component, ViewChild } from '@angular/core';\nimport { UntypedFormGroup } from '@angular/forms';\nimport { ImageTransformationService } from '@vendasta/image-transformation';\n\n@Component({\n selector: 'store-order-field',\n styleUrls: ['./field.component.scss'],\n templateUrl: './field.component.html',\n})\nexport class FieldComponent {\n @Input() field: any; // avoid checking union type in template\n @Input() form: UntypedFormGroup;\n readonly MAX_FILE_SIZE = 26214400; // 25MB\n\n @ViewChild('glxyUploader') glxyUploader;\n\n constructor(private imageTransformationService: ImageTransformationService) {}\n\n get isValid(): boolean {\n return this.form.controls[this.field.id].valid || this.form.controls[this.field.id].pristine;\n }\n\n showCheckboxError(field): boolean {\n return (\n this.form.controls[field].errors && this.form.controls[field].errors.required && this.form.controls[field].touched\n );\n }\n\n getValue(): any {\n return this.form.controls[this.field.id].value;\n }\n\n dropdownAnswer(): string {\n const answer = this.getValue();\n return ((this.field.options || []).find((option) => option.value === answer) || { label: answer }).label;\n }\n\n multidropdownAnswer(): string {\n const answer = this.getValue();\n return (answer || [])\n .map((a) => ((this.field.options || []).find((option) => option.value === a) || { name: a }).name)\n .join(', ');\n }\n\n checkboxAnswer(): string {\n const answer = this.getValue();\n return answer ? 'Yes' : 'No';\n }\n\n fileUploadAnswer(): string {\n const answer = this.getValue() || [];\n return answer.map((a) => (a.name ? a.name : a.url ? a.url : a)).join(', ');\n }\n\n // Handle deleting file from both galaxy uploader component, and order-form form values\n deleteFile(event: any): void {\n const filterOutDeletedFile = this.form.controls[this.field.id].value.filter(\n (file) => file.name !== event.name && file.url !== event.url,\n );\n\n this.form.controls[this.field.id].setValue(filterOutDeletedFile);\n // Tell galaxy uploader about delete so it can handle maxFiles properly\n this.glxyUploader.deleteFile(event);\n }\n\n // Manually update order-form form with events from galaxy file uploader\n onUpload(event: any): void {\n const uploadedFiles = this.form.controls[this.field.id].value;\n let fileUrl = event.url;\n // retain original scale for images\n if (this.imageTransformationService.isServingUrl(event.url)) {\n fileUrl += '=s0';\n }\n\n uploadedFiles.push({\n name: event.name,\n url: fileUrl,\n });\n\n this.form.controls[this.field.id].setValue(uploadedFiles);\n }\n}\n", "\n \n
\n {{ 'FRONTEND.STORE.ORDER_FORM.HIDDEN_FROM_END_USERS' | translate }}:\n
\n
\n {{ 'FRONTEND.STORE.ORDER_FORM.OFFICE_EDITABLE_ONLY' | translate }}:\n
\n\n \n \n
\n \n {{ field.label | translate }}{{ field.required ? '*' : '' }}\n \n \n {{ option.label }}\n \n \n \n {{ field.description }}\n \n \n
\n
\n
{{ field.label | translate }}:
\n
{{ dropdownAnswer() }}
\n
\n
\n \n
\n \n
\n
\n
{{ field.label | translate }}:
\n
{{ multidropdownAnswer() }}
\n
\n
\n
\n\n \n
\n \n {{ field.label | translate }}{{ field.required ? ' *' : '' }}\n \n \n {{ field.description }}\n \n \n {{ field.regexErrorMessage }}\n \n \n
\n
\n
{{ field.label | translate }}:
\n
{{ getValue() }}
\n
\n
\n\n \n
\n \n \n {{ field.label | translate }}{{ field.required ? ' *' : '' }}\n \n {{ field.description }}\n \n \n \n {{ 'FRONTEND.STORE.REQUIRED' | translate }}\n \n \n
\n
\n
{{ field.label | translate }}:
\n
{{ checkboxAnswer() }}
\n
\n
\n\n \n \n \n {{ field.label | translate }}{{ field.required ? ' *' : '' }}\n \n \n {{ field.description }}\n \n \n \n
\n
{{ field.label | translate }}:
\n
{{ getValue() }}
\n
\n
\n\n \n \n \n {{ field.label | translate }}{{ field.required ? ' *' : '' }}\n \n {{ field.description }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n
{{ field.label | translate }}:
\n
{{ fileUploadAnswer() }}
\n
\n
\n\n \n
\n \n {{ field.label | translate }}{{ field.required ? ' *' : '' }}\n \n \n {{ option.label | translate }}\n \n \n \n {{ field.description }}\n \n \n
\n
\n
{{ field.label | translate }}:
\n
{{ dropdownAnswer() }}
\n
\n
\n
\n
\n", "import { Component, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';\nimport { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';\nimport { TranslateService } from '@ngx-translate/core';\nimport { Subscription, combineLatest } from 'rxjs';\nimport { debounceTime, skip } from 'rxjs/operators';\nimport { FieldBase } from '../fields/field-base';\nimport { Option } from '../fields/field-dropdown';\nimport { VBCUserField } from '../fields/field-vbcuser';\nimport { TemporaryStorageService } from './temporary-storage.service';\n\n@Component({\n selector: 'store-dropdown-form-section',\n template: `\n \n \n \n
\n \n {{ iconName }}\n \n
{{ titleText }}
\n \n
{{ titleText }}
\n
\n {{ autoTitleText }}\n
\n
\n \n
\n \n
\n {{ autoDescriptionText }}\n
\n
\n {{ descriptionText }}\n
\n
\n
\n
\n \n

\n {{ editingHint }}\n

\n
\n \n
\n
\n
{{ titleText }}
\n \n
\n `,\n styleUrls: ['./dropdown-form-section.component.scss'],\n})\nexport class DropDownFormSectionComponent implements OnInit, OnDestroy {\n @Input() prepopulatedData: object;\n @Input() startOpen = false;\n @Input() parentForm: UntypedFormGroup;\n @Input() displayAutoTitle = false;\n @Input() titleText = '';\n @Input() displayAutoDescription = true;\n @Input() descriptionText = '';\n @Input() fields: FieldBase[];\n @Input() editingHint = '';\n @Input() expandable = true;\n @Input() temporaryStorageKey = '';\n @Input() bottomActionsContent: TemplateRef;\n\n autoDescriptionText = '';\n iconName = 'help_outline';\n subscriptions: Subscription[] = [];\n uniqueIndex = 1;\n autoTitleText = '';\n contactIdField: VBCUserField;\n\n constructor(\n private translateService: TranslateService,\n private temporaryStorage: TemporaryStorageService,\n ) {}\n\n ngOnInit(): void {\n const allFieldsHidden = this.fields.reduce((p, c) => p && c.hidden, true);\n if (allFieldsHidden) {\n this.expandable = false;\n }\n while (Object.prototype.hasOwnProperty.call(this.parentForm.controls, this.titleText + this.uniqueIndex)) {\n this.uniqueIndex += 1;\n }\n\n this.parentForm.addControl(this.titleText + this.uniqueIndex, this.toFormGroup(this.fields));\n const form = this.parentForm.controls[this.titleText + this.uniqueIndex] as UntypedFormGroup;\n\n // Check if a field with id 'contact_id' exists\n // This is only used within the common form data when the vendor has enabled the \"contact\" data fields\n // If the business has more than a single smb user then a dynamic dropdown will be displayed\n // Show the form as open so that the data can be selected\n this.contactIdField = this.fields.find((field) => field.id === 'contact_id') as VBCUserField;\n\n // set form values with prepopulated field data\n for (const key in form.controls) {\n if (Object.prototype.hasOwnProperty.call(form.controls, key)) {\n if (this.prepopulatedData != null && Object.prototype.hasOwnProperty.call(this.prepopulatedData, key)) {\n form.controls[key].setValue(this.prepopulatedData[key]);\n }\n }\n }\n\n // temporary storage key to use for session storage\n if (this.temporaryStorageKey != '') {\n // check to see if an existing value exists in the temporary storage\n const existingFormData = this.temporaryStorage.get(this.temporaryStorageKey);\n if (existingFormData) {\n for (const key in form.controls) {\n // if the form control exists check to see if it has an existing value (that was previously saved)\n const field = this.fields.find((f) => f.id === key);\n if (field?.value) {\n form.controls[key].setValue(field.value);\n } else {\n // there is no existing value set the value from the temporary storage\n if (Object.prototype.hasOwnProperty.call(form.controls, key)) {\n if (Object.prototype.hasOwnProperty.call(existingFormData, key)) {\n form.controls[key].setValue(existingFormData[key]);\n }\n }\n }\n }\n }\n\n // save the latest form values to temporary storage\n this.subscriptions.push(\n form.valueChanges.pipe(skip(1), debounceTime(300)).subscribe((value) => {\n this.temporaryStorage.set(this.temporaryStorageKey, value);\n }),\n );\n }\n\n this.subscriptions.push(\n combineLatest([form.statusChanges, this.translateService.stream('FRONTEND.STORE.AUTO_TITLE_TEXT')]).subscribe(\n ([change, autoTitleText]) => {\n let missingFields = false;\n for (const control in form.controls) {\n if (this.isFormControlEmpty(form, control)) {\n this.iconName = 'help_outline';\n missingFields = true;\n if (change === 'INVALID') {\n this.autoTitleText = autoTitleText.SOME_FIELDS_INVALID;\n this.startOpen = true;\n if (form.controls[control].dirty || form.controls[control].touched) {\n this.iconName = 'warning';\n break;\n }\n } else {\n this.autoTitleText = autoTitleText.OPTIONAL_FIELDS_UNANSWERED;\n }\n }\n }\n if (!missingFields) {\n this.iconName = 'check_circle';\n this.autoTitleText = autoTitleText.COMPLETE;\n }\n },\n ),\n );\n\n // The drop down form section is used by the common form data section as well as vendor order forms\n // The following code handles the \"contact_id\" fields which is part of the common order form data\n // TODO: refactor the common order form data into dedicated components\n if (this.contactIdField) {\n this.subscriptions.push(\n form.get('contact_id').valueChanges.subscribe((contactIdValue) => {\n const selectedContact = JSON.parse(contactIdValue);\n\n if (selectedContact.firstName) {\n let fullName = selectedContact.firstName;\n if (selectedContact.lastName) {\n fullName += ' ' + selectedContact.lastName;\n }\n form.controls['contact_name'].setValue(fullName, { emitEvent: true });\n }\n\n if (selectedContact.email) {\n form.controls['contact_email'].setValue(selectedContact.email, { emitEvent: true });\n }\n\n if (selectedContact.phoneNumber) {\n form.controls['contact_phone_number'].setValue(selectedContact.phoneNumber, { emitEvent: true });\n }\n }),\n );\n }\n\n this.subscriptions.push(\n form.valueChanges.subscribe((changes) => {\n let description = '';\n for (const key in changes) {\n // As per above we need to skip change detection for the contact_id field\n if (key === 'contact_id') {\n continue;\n }\n\n if (Object.prototype.hasOwnProperty.call(changes, key) && changes[key] != null && changes[key].length > 0) {\n if (changes[key][0].name) {\n for (const fileKey in changes[key]) {\n if (changes[key][fileKey] != null) {\n description += changes[key][fileKey].name;\n description += ', ';\n }\n }\n } else {\n description += changes[key];\n description += ', ';\n }\n }\n }\n if (this.displayAutoDescription) {\n this.autoDescriptionText = description.substring(0, description.length - 2);\n }\n }),\n );\n\n if (this.contactIdField) {\n this.startOpen = true;\n const contacts = this.contactIdField.options as Option[];\n const validContact = contacts.find((contact) => {\n if (contact?.disabled) {\n return false;\n }\n const currentContact = JSON.parse(contact.value);\n return currentContact.email === form.controls['contact_email'].value;\n });\n if (validContact) {\n form.controls['contact_id'].setValue(validContact.value, { emitEvent: true });\n }\n }\n\n form.updateValueAndValidity({ onlySelf: false, emitEvent: true });\n }\n\n toFormGroup(formFields: FieldBase[]): UntypedFormGroup {\n const group: any = {};\n formFields.forEach((field) => {\n let validations;\n if (!field.required || field.hidden) {\n validations = field.validator;\n } else if (field.controlType === 'checkbox') {\n validations = [Validators.requiredTrue, field.validator];\n } else {\n validations = [Validators.required, field.validator];\n }\n\n const formControl = new UntypedFormControl({ value: field.value, disabled: field.disabled }, validations);\n group[field.id] = formControl;\n this.subscriptions.push(formControl.valueChanges.subscribe((value) => (field.value = value)));\n });\n\n return new UntypedFormGroup(group);\n }\n\n isFormControlEmpty(form: UntypedFormGroup, control: string): boolean {\n // hidden fields should not need to be filled in\n if (this.fields.find((field) => field.id === control)?.hidden) {\n return false;\n }\n\n return (\n !form.controls[control].value ||\n (form.controls[control].value.constructor === Array && !form.controls[control].value[0])\n );\n }\n\n ngOnDestroy(): void {\n this.parentForm.removeControl(this.titleText + this.uniqueIndex);\n this.subscriptions.forEach((subscription) => subscription.unsubscribe());\n }\n}\n", "import { ValidatorFn } from '@angular/forms';\n\nexport const FILES = 'file';\nexport const DROP_DOWN = 'dropdown';\nexport const CHECK_BOX = 'checkbox';\nexport const TEXT_AREA = 'textarea';\nexport const TEXT_BOX = 'textbox';\nexport const BUSINESS_USER = 'vbcuser';\n\nexport type ControlType = 'checkbox' | 'dropdown' | 'file' | 'textbox' | 'textarea' | 'vbcuser';\n\nexport interface FieldBaseOptions {\n // TODO This and marketplace-apps OrderFormFieldInterface should share a common base\n id: string;\n label: string;\n required?: boolean;\n description?: string;\n type?: string;\n prefix?: string;\n suffix?: string;\n regexValidator?: string;\n regexErrorMessage?: string;\n disabled?: boolean;\n forOfficeUseOnly?: boolean;\n officeEditableOnly?: boolean;\n hidden?: boolean;\n displayOnly?: boolean;\n}\n\nexport abstract class FieldBase implements FieldBaseOptions {\n value: T;\n id: string;\n label: string;\n required: boolean;\n order: number;\n description: string;\n controlType: ControlType;\n prefix: string;\n suffix: string;\n regexValidator: string;\n regexErrorMessage: string;\n disabled: boolean;\n validator: ValidatorFn;\n forOfficeUseOnly: boolean;\n officeEditableOnly: boolean;\n hidden: boolean;\n displayOnly: boolean;\n\n constructor(options: FieldBaseOptions) {\n this.id = options.id;\n this.label = options.label;\n this.required = !!options.required;\n this.description = options.description || null;\n options.type = options.type === 'text' ? 'textbox' : options.type;\n this.controlType = options.type as ControlType;\n this.prefix = options.prefix;\n this.suffix = options.suffix;\n this.regexValidator = options.regexValidator;\n this.regexErrorMessage = options.regexErrorMessage;\n this.disabled = options.disabled || false;\n this.validator = this.validatorBuilder();\n this.forOfficeUseOnly = !!options.forOfficeUseOnly;\n this.officeEditableOnly = !!options.officeEditableOnly;\n this.hidden = options.hidden;\n this.displayOnly = options.displayOnly;\n }\n\n protected validatorBuilder(): ValidatorFn {\n return (): { [key: string]: any } => {\n return null;\n };\n }\n}\n", "import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';\nimport {\n AbstractControl,\n UntypedFormArray,\n UntypedFormControl,\n UntypedFormGroup,\n ValidatorFn,\n Validators,\n} from '@angular/forms';\nimport { OrderFormFieldInterface, OrderFormFieldOptionInterface } from '@galaxy/marketplace-apps/v1';\nimport { BUSINESS_USER, CHECK_BOX, ControlType, DROP_DOWN, FILES, TEXT_AREA, TEXT_BOX } from '../field-base';\n\n@Component({\n selector: 'store-field-builder',\n templateUrl: './field-builder.component.html',\n styleUrls: ['./field-builder.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FieldBuilderComponent implements OnInit {\n @Input() orderFormFieldGroup: UntypedFormGroup;\n @Input() supportedFieldTypes: FormFieldInterface[];\n @Output() removeField: EventEmitter = new EventEmitter(null);\n\n showValidators: boolean;\n showSuffixes: boolean;\n\n static createDropDownOption(option: OrderFormFieldOptionInterface, required: boolean): UntypedFormGroup {\n return new UntypedFormGroup({\n label: new UntypedFormControl(option.label || option.value, required ? [Validators.required] : []),\n value: new UntypedFormControl(option.value),\n useValue: new UntypedFormControl(!!option.value),\n });\n }\n\n static createDropDownOptions(options: OrderFormFieldOptionInterface[]): UntypedFormArray {\n if (options === null || options === undefined) {\n options = [];\n }\n const orderFormFieldsGroupArray: UntypedFormGroup[] = options.map((option) =>\n FieldBuilderComponent.createDropDownOption(option, false),\n );\n return new UntypedFormArray(orderFormFieldsGroupArray);\n }\n\n static createOrderFormFieldGroup(orderFormField: OrderFormFieldInterface): UntypedFormGroup {\n const orderFormFieldGroup = new UntypedFormGroup({\n descriptionControl: new UntypedFormControl(orderFormField.description, []),\n labelControl: new UntypedFormControl(orderFormField.label, [Validators.required]),\n idControl: new UntypedFormControl(orderFormField.id, [\n Validators.required,\n Validators.pattern(\"^[\\\\w\\\\s-$&`:?/.,()+*'’|]+$\"),\n ]),\n requiredControl: new UntypedFormControl(orderFormField.required || false, []),\n uploadUrlControl: new UntypedFormControl(orderFormField.uploadUrl, []),\n prefixControl: new UntypedFormControl(orderFormField.prefix, []),\n suffixControl: new UntypedFormControl(orderFormField.suffix, []),\n regexValidatorControl: new UntypedFormControl(orderFormField.regexValidator, regexValidator()),\n regexErrorMessageControl: new UntypedFormControl(orderFormField.regexErrorMessage, []),\n typeControl: new UntypedFormControl(getFieldInterface(orderFormField.type), [Validators.required]),\n allowMultiples: new UntypedFormControl(orderFormField.allowMultiples || false, []),\n allowDuplicates: new UntypedFormControl(orderFormField.allowDuplicates || false, []),\n maxChoices: new UntypedFormControl(orderFormField.maxChoices ? orderFormField.maxChoices : 1, [\n Validators.min(1),\n numberValidator(),\n ]),\n forOfficeUseOnly: new UntypedFormControl(orderFormField.forOfficeUseOnly || false, []),\n officeEditableOnly: new UntypedFormControl(orderFormField.officeEditableOnly || false, []),\n });\n if (orderFormField.optionsWithLabels) {\n const optionsArray = FieldBuilderComponent.createDropDownOptions(orderFormField.optionsWithLabels);\n orderFormFieldGroup.addControl('optionsFormArray', optionsArray);\n } else if (orderFormField.options) {\n const optionsArray = FieldBuilderComponent.createDropDownOptions(\n orderFormField.options.map((option) => ({ label: option })),\n );\n orderFormFieldGroup.addControl('optionsFormArray', optionsArray);\n } else {\n orderFormFieldGroup.addControl('optionsFormArray', new UntypedFormArray([]));\n }\n\n return orderFormFieldGroup;\n }\n\n ngOnInit(): void {\n if (!this.supportedFieldTypes || this.supportedFieldTypes.length < 1) {\n this.supportedFieldTypes = FORM_FIELDS;\n }\n this.initForm();\n }\n\n initForm(): void {\n // Check to see if form has been initiated\n if (!this.orderFormFieldGroup.get('labelControl')) {\n this.orderFormFieldGroup = FieldBuilderComponent.createOrderFormFieldGroup({});\n }\n }\n\n getOptionsArray(): UntypedFormArray {\n return this.orderFormFieldGroup.get('optionsFormArray') as UntypedFormArray;\n }\n\n getCurrentFieldInterface(): string {\n const formFieldTypeInterface = this.orderFormFieldGroup.get('typeControl').value as FormFieldInterface;\n return formFieldTypeInterface ? formFieldTypeInterface.id : null;\n }\n\n createIdFromLabel(): void {\n const idControlValue = this.orderFormFieldGroup.get('idControl').value;\n if (!idControlValue || idControlValue === '') {\n const newId = convertLabelInputIntoId(this.orderFormFieldGroup.get('labelControl').value);\n this.orderFormFieldGroup.get('idControl').setValue(newId);\n }\n }\n\n removeOption(index: number): void {\n const formArray = this.getOptionsArray();\n formArray.removeAt(index);\n }\n\n addOption(): void {\n const fieldArray = this.getOptionsArray();\n fieldArray.push(FieldBuilderComponent.createDropDownOption({ label: '' }, true));\n }\n\n getFormValues(): OrderFormFieldInterface {\n const obj: OrderFormFieldInterface = {};\n obj.label = this.orderFormFieldGroup.get('labelControl').value;\n obj.id = this.orderFormFieldGroup.get('idControl').value;\n\n const formFieldType = this.orderFormFieldGroup.get('typeControl').value as FormFieldInterface; // use the id instead of the whole object\n obj.type = formFieldType ? formFieldType.id : null;\n\n // Options will be nested under their own form control in the larger Orderformfield array\n const optionsArray = this.orderFormFieldGroup.get('optionsFormArray') as UntypedFormArray;\n if (optionsArray && optionsArray.value) {\n obj.options = optionsArray.value.map((option) => option.value || option.label);\n obj.optionsWithLabels = optionsArray.value.map((option) => ({ label: option.label, value: option.value }));\n }\n\n obj.description = this.orderFormFieldGroup.get('descriptionControl').value;\n obj.required = this.orderFormFieldGroup.get('requiredControl').value || false;\n obj.uploadUrl = this.orderFormFieldGroup.get('uploadUrlControl').value;\n obj.prefix = this.orderFormFieldGroup.get('prefixControl').value;\n obj.suffix = this.orderFormFieldGroup.get('suffixControl').value;\n obj.regexValidator = this.orderFormFieldGroup.get('regexValidatorControl').value;\n obj.regexErrorMessage = this.orderFormFieldGroup.get('regexErrorMessageControl').value;\n obj.allowMultiples = this.orderFormFieldGroup.get('allowMultiples').value || false;\n obj.allowDuplicates = this.orderFormFieldGroup.get('allowDuplicates').value || false;\n obj.maxChoices = this.orderFormFieldGroup.get('maxChoices').value || 1;\n obj.forOfficeUseOnly = this.orderFormFieldGroup.get('forOfficeUseOnly').value || false;\n obj.officeEditableOnly = this.orderFormFieldGroup.get('officeEditableOnly').value || false;\n return obj;\n }\n}\n\n// Validation to ensure supplied regex is valid\nexport function regexValidator(): ValidatorFn {\n return (control: AbstractControl): { [key: string]: any } => {\n try {\n new RegExp(control.value || '');\n } catch (e) {\n return { regex: true };\n }\n return null;\n };\n}\n\nexport function numberValidator(): ValidatorFn {\n return (control: AbstractControl): { [key: string]: any } => {\n const isNumber = /^\\d+$/;\n if (isNumber.test(control.value)) {\n return null;\n }\n return { number: true };\n };\n}\n\nexport interface FormFieldInterface {\n id: ControlType;\n name: string;\n}\n\nexport const FORM_FIELDS: FormFieldInterface[] = [\n { id: FILES, name: 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPES.FILES' },\n { id: DROP_DOWN, name: 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPES.DROP_DOWN' },\n { id: CHECK_BOX, name: 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPES.CHECK_BOX' },\n { id: TEXT_AREA, name: 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPES.TEXT_AREA' },\n { id: TEXT_BOX, name: 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPES.TEXT_BOX' },\n { id: BUSINESS_USER, name: 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPES.END_USER' },\n];\n\nexport function getFieldInterface(formFieldId: string): FormFieldInterface {\n return getFormFieldInterfaceFromId(formFieldId as ControlType);\n}\n\nexport function getFormFieldInterfaceFromId(id: ControlType): FormFieldInterface {\n return FORM_FIELDS.find((obj: FormFieldInterface) => obj.id === id);\n}\n\nexport function convertLabelInputIntoId(labelValue: string): string {\n let newId = labelValue.toLowerCase();\n newId = newId.replace(/[ -]/g, '_');\n newId = newId.replace(/[^0-9a-z_]/g, '');\n return newId;\n}\n", "\n
\n \n {{ 'FRONTEND.STORE.REQUIRED' | translate }}\n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.HIDDEN_FROM_END_USERS' | translate }}\n \n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.OFFICE_EDITABLE_ONLY' | translate }}\n \n \n
\n \n \n \n {{ field.name | translate }}\n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.FIELD_TYPE_REQUIRED' | translate }}\n \n \n \n \n \n
\n \n \n \n \n \n \n \n + {{ 'FRONTEND.STORE.ORDER_FORM.ASSIGN_VALUE' | translate }}\n \n \n \n clear\n \n \n
\n \n \n
\n \n {{ 'FRONTEND.STORE.ORDER_FORM.ALLOW_MULTIPLES' | translate }}\n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.ALLOW_DUPLICATES' | translate }}\n \n
\n \n \n \n {{\n 'FRONTEND.STORE.ORDER_FORM.CHOICES_HINT'\n | translate: { minChoices: 0 }\n }}\n \n \n {{\n 'FRONTEND.STORE.ORDER_FORM.CHOICES_ERROR'\n | translate: { minChoices: 0 }\n }}\n \n \n
\n
\n
\n \n
\n \n \n \n {{\n 'FRONTEND.STORE.ORDER_FORM.LABEL_HINT'\n | translate: { maxCharacters: 140 }\n }}\n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.LABEL_ERROR' | translate }}\n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n
\n \n + {{ 'FRONTEND.STORE.ORDER_FORM.PREFIX_SUFFIX' | translate }}\n \n
\n - {{ 'FRONTEND.STORE.ORDER_FORM.HIDDEN_NOTE' | translate }}\n
\n
\n \n\n
\n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.ID_HINT' | translate }}\n \n {{ 'FRONTEND.STORE.ORDER_FORM.ID_ERROR' | translate }}\n \n \n
\n\n \n \n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.UPLOAD_URL_HINT' | translate }}\n \n \n \n\n \n \n \n \n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.REGEX_VALIDATOR_ERROR' | translate }}\n \n \n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.REGEX_ERROR_MESSAGE_HINT' | translate }}\n \n \n \n \n
\n \n + {{ 'FRONTEND.STORE.ORDER_FORM.VALIDATION' | translate }}\n \n
\n - {{ 'FRONTEND.STORE.ORDER_FORM.HIDDEN_NOTE' | translate }}\n
\n
\n \n\n \n
\n \n \n \n {{ 'FRONTEND.STORE.ORDER_FORM.HINT_TEXT_HINT' | translate }}\n \n \n
\n\n
\n \n {{ 'FRONTEND.STORE.ORDER_FORM.DELETE_FIELD' | translate }}\n \n \n
\n\n", "import { Injectable } from '@angular/core';\nimport { FieldBase } from './field-base';\nimport { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';\nimport { Subscription } from 'rxjs';\n\n@Injectable()\nexport class FieldService {\n createFormControl(field: FieldBase): UntypedFormControl {\n if (field.required) {\n return new UntypedFormControl(field.value, Validators.required);\n } else {\n return new UntypedFormControl(field.value);\n }\n }\n\n toFormGroup(formFields: FieldBase[], subscriptions: Subscription[]): UntypedFormGroup {\n const group: any = {};\n formFields.forEach((field) => {\n let formControl: UntypedFormControl;\n if (field.controlType === 'checkbox') {\n formControl = new UntypedFormControl(field.value);\n } else {\n formControl = this.createFormControl(field);\n }\n group[field.id] = formControl;\n if (subscriptions) {\n subscriptions.push(formControl.valueChanges.subscribe((value) => (field.value = value)));\n }\n });\n return new UntypedFormGroup(group);\n }\n}\n", "import { FieldBase } from '../fields/field-base';\nimport { UntypedFormGroup } from '@angular/forms';\n\nexport class DropDownFormSectionData {\n prepopulatedData: any;\n startOpen: boolean;\n parentForm: UntypedFormGroup;\n displayAutoTitle: boolean;\n titleText: string;\n displayAutoDescription: boolean;\n descriptionText: string;\n fields: FieldBase[];\n editingHint: string;\n /* tslint:disable */\n expandable? = true;\n /* tslint:enable */\n}\n", "import { UntypedFormGroup } from '@angular/forms';\nimport { DropDownFormSectionData } from '../dropdown-form-section/dropdown-form-section-data';\nexport class OrderFormSectionData {\n parentForm: UntypedFormGroup;\n titleText: string;\n subtitleText: string;\n descriptionText: string;\n iconUrl: string;\n primarySection: DropDownFormSectionData;\n subsections: DropDownFormSectionData[];\n footerText?: string;\n}\n", "import { DropDownFormSectionData } from '../dropdown-form-section/dropdown-form-section-data';\nimport { OrderFormSectionData } from '../order-form-section/order-form-section-data';\nimport { FieldBase, FieldBaseOptions } from '../fields/field-base';\nimport { DropDownFieldOptions } from '../fields/field-dropdown';\nimport { FileUploadGroupFieldOptions } from '../fields/field-fileuploadgroup';\nimport { TextBoxFieldOptions } from '../fields/field-textbox';\nimport { User, VBCUserFieldOptions } from '../fields/field-vbcuser';\n\nexport type OrderFormFieldOptionsType =\n | FieldBaseOptions\n | DropDownFieldOptions\n | FileUploadGroupFieldOptions\n | TextBoxFieldOptions\n | VBCUserFieldOptions;\n\nexport interface OrderFormInterface {\n appId: string;\n addonId?: string;\n commonForm?: IncludedCommonFormFieldsInterface;\n commonFormRequiredFields?: IncludedCommonFormFieldsInterface;\n customFields?: OrderFormFieldOptionsType[];\n footerText?: string;\n}\n\nexport interface IncludedCommonFormFieldsInterface {\n businessName?: boolean;\n businessAddress?: boolean;\n businessPhoneNumber?: boolean;\n businessAccountGroupId?: boolean;\n businessWebsite?: boolean;\n salespersonName?: boolean;\n salespersonPhoneNumber?: boolean;\n salespersonEmail?: boolean;\n contactName?: boolean;\n contactPhoneNumber?: boolean;\n contactEmail?: boolean;\n}\n\nexport interface CommonFormSectionInterface {\n fields: CommonFormFieldInterface[];\n sectionName?: string;\n populatedData?: CommonFormData;\n}\n\nexport interface AddonKeyInterface {\n addonId?: string;\n appId?: string;\n}\n\nexport interface CommonFormFieldInterface extends FieldBase {\n productIds?: string[];\n addonKeys?: AddonKeyInterface[];\n}\n\nexport interface CommonFieldIdsInterface {\n fieldId: string;\n required: boolean;\n}\n\n// Keys are snake cased here because that is the format in which we submit this data to vendors\nexport interface CommonFormData {\n user_options?: User[];\n contact_email?: string;\n contact_name?: string;\n contact_phone_number?: string;\n business_account_group_id?: string;\n business_name?: string;\n business_address?: string;\n business_phone_number?: string;\n business_website?: string;\n salesperson_name?: string;\n salesperson_email?: string;\n salesperson_phone_number?: string;\n}\n\nexport class ProductOrderFormSectionData extends OrderFormSectionData {\n businessId: string;\n productId: string;\n subsections: AddonDropDownFormSectionData[];\n}\n\nexport class AddonDropDownFormSectionData extends DropDownFormSectionData {\n productId: string;\n addonId: string;\n}\n\nexport interface ProductInfoInterface {\n productId: string;\n name?: string;\n tagline?: string;\n icon?: string;\n addonInfo?: AddonInfoInterface[];\n}\n\nexport interface AddonInfoInterface {\n addonId: string;\n name?: string;\n}\n\nexport interface CustomFieldsAnswers {\n productID: string;\n addonKey?: AddonKeyInterface;\n customFieldsAnswers: CustomFieldsAnswer[];\n}\n\nexport interface CustomFieldsAnswer {\n fieldId: string;\n answer?: string;\n displayOnly?: boolean;\n}\n\nexport interface OrderFormOptionsInterface {\n bypassRequiredQuestions?: boolean;\n readOnly?: boolean;\n showOfficeUseQuestions?: boolean;\n}\n", "import { Pipe, PipeTransform } from '@angular/core';\n\n@Pipe({\n name: 'buildTemporaryStorageKey',\n standalone: true,\n})\nexport class BuildTemporaryStorageKeyPipe implements PipeTransform {\n transform(args: { businessId: string; id: string; index?: number }): string {\n return buildTemporaryStorageKey(args);\n }\n}\n\nexport function buildTemporaryStorageKey(args: { businessId: string; id: string; index?: number }): string {\n if (!args) {\n return '';\n }\n const parts = [args.businessId, args.id];\n const indexAsString = args.index?.toString();\n if (indexAsString) {\n parts.push(indexAsString);\n }\n return parts.join('-');\n}\n", "import { Component, Input, OnInit, TemplateRef } from '@angular/core';\nimport { DropDownFormSectionData } from '../dropdown-form-section/dropdown-form-section-data';\nimport { ProductOrderFormSectionData } from '../order-form/interface';\n\n@Component({\n selector: 'store-order-form-section',\n templateUrl: 'order-form-section.component.html',\n styleUrls: ['./order-form-section.component.scss'],\n})\nexport class OrderFormSectionComponent implements OnInit {\n @Input() data: ProductOrderFormSectionData;\n @Input() extraHeaderContent: TemplateRef;\n @Input() bottomActionsContent: TemplateRef;\n\n ngOnInit(): void {\n // The first section should always be expanded.\n const topSection = this.getTopSection();\n if (topSection) {\n topSection.startOpen = true;\n }\n }\n\n getTopSection(): DropDownFormSectionData {\n if (!this.data) {\n return null;\n }\n if (this.data.primarySection) {\n return this.data.primarySection;\n }\n if (this.data.subsections && this.data.subsections.length > 0) {\n return this.data.subsections[0];\n }\n return null;\n }\n}\n", "
\n
\n
\n
\n \n
\n

{{ data.titleText }}

\n \n {{ data.subtitleText }}\n \n
\n
\n
\n {{ data.descriptionText }}\n
\n \n
\n
\n \n 0\">\n \n \n \n \n \n

Additional information

\n
\n
\n
\n
\n
\n \n
\n
\n", "import { ControlType, FieldBase } from './field-base';\n\nexport class CheckboxField extends FieldBase {\n readonly controlType: ControlType = 'checkbox';\n}\n", "import { ControlType, FieldBase, FieldBaseOptions } from './field-base';\nimport { Option as InputOption } from '@vendasta/forms';\nimport { AbstractControl, ValidatorFn } from '@angular/forms';\n\nexport interface Option {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface DropDownFieldOptions extends FieldBaseOptions {\n options: Option[] | InputOption[] | string[];\n allowMultiples?: boolean;\n allowDuplicates?: boolean;\n maxChoices?: number;\n}\n\nexport class DropdownField extends FieldBase {\n readonly controlType: ControlType = 'dropdown';\n options: Option[] | InputOption[];\n allowMultiples: boolean;\n allowDuplicates: boolean;\n maxChoices: number;\n\n constructor(options: DropDownFieldOptions) {\n super(options);\n this.options = [];\n if (!!options.options && options.options.length > 0) {\n if (typeof options.options[0] === 'string') {\n if (options.allowMultiples) {\n this.options = options.options.map((o) => { value: o, name: o });\n } else {\n this.options = options.options.map((o) =>