{ "version": 3, "sources": ["libs/orders/src/lib/core/tokens.ts", "libs/orders/src/lib/core/orders.service.ts", "libs/orders/src/lib/assets/en_devel.json", "libs/orders/src/lib/components/agreements/agreements.component.ts", "libs/orders/src/lib/components/agreements/agreements.component.html", "libs/orders/src/lib/components/attachments/attachments-dialog.component.ts", "libs/orders/src/lib/components/attachments/attachments-dialog.component.html", "libs/orders/src/lib/components/attachments/attachments.component.ts", "libs/orders/src/lib/components/attachments/attachments.component.html", "libs/orders/src/lib/components/create-order-details/create-order-details.component.ts", "libs/orders/src/lib/components/create-order-details/create-order-details.component.html", "libs/orders/src/lib/shared/constants.ts", "libs/orders/src/lib/shared/utils.ts", "libs/orders/src/lib/utils.ts", "libs/orders/src/lib/components/order-form/order-form.service.ts", "libs/orders/src/lib/components/order-form/order-form.component.ts", "libs/orders/src/lib/components/order-form/order-form.component.html", "libs/orders/src/lib/components/notes/notes.component.ts", "libs/orders/src/lib/components/notes/notes.component.html", "libs/orders/src/lib/components/tags/tags.component.ts", "libs/orders/src/lib/components/tags/tags.component.html", "libs/orders/src/lib/components/line-items/line-items-adapter.service.ts", "libs/orders/src/lib/components/line-items/line-items.service.ts", "libs/orders/src/lib/components/line-items/utils.ts", "libs/orders/src/lib/components/line-items/line-items.component.ts", "libs/orders/src/lib/components/line-items/line-items.component.html", "libs/orders/src/lib/components/currency-selector/currency-selector.component.ts", "libs/orders/src/lib/components/currency-selector/currency-selector.component.html", "libs/orders/src/lib/components/variable-prices/variable-prices.component.ts", "libs/orders/src/lib/components/variable-prices/variable-prices.component.html", "libs/orders/src/lib/components/create-order/create/create-order.component.ts", "libs/orders/src/lib/components/create-order/create/create-order.component.html", "libs/orders/src/lib/stripe.service.ts", "libs/orders/src/lib/components/edit-order/edit-order.component.ts", "libs/orders/src/lib/components/edit-order/edit-order.component.html", "libs/orders/src/lib/components/fulfillment/order-fulfillment.interface.ts", "libs/orders/src/lib/components/history/history.service.ts", "libs/orders/src/lib/components/history/history.component.ts", "libs/orders/src/lib/components/history/history.component.html", "libs/orders/src/lib/components/line-items/line-items-selector.service.ts", "libs/orders/src/lib/components/order-actions/order-actions.component.ts", "libs/orders/src/lib/components/order-actions/order-actions.component.html", "libs/orders/src/lib/components/order-form/custom-price-form/custom-price-form.component.ts", "libs/orders/src/lib/components/order-form/custom-price-form/custom-price-form.component.html", "libs/orders/src/lib/components/order-submitted/order-submitted.service.ts", "libs/orders/src/lib/components/order-submitted/order-submitted.component.ts", "libs/orders/src/lib/components/order-submitted/order-submitted.component.html", "libs/orders/src/lib/shared/status-badge.ts", "libs/orders/src/lib/components/view-order/work-order-details-helper.service.ts", "libs/orders/src/lib/components/view-order/view-order.component.ts", "libs/orders/src/lib/components/view-order/view-order.component.html", "libs/orders/src/lib/components/view-or-edit-order/view-or-edit-order.component.ts", "libs/orders/src/lib/components/view-or-edit-order/view-or-edit-order.component.html", "libs/orders/src/lib/orders.module.ts", "libs/orders/src/lib/app-requirements.ts"], "sourcesContent": ["import { InjectionToken } from '@angular/core';\nimport { AccountGroup, ProjectionFilter, ReadFilter } from '@galaxy/account-group';\nimport { Package } from '@vendasta/marketplace-packages';\nimport { User } from '@vendasta/store';\nimport { Observable } from 'rxjs';\n\n// Must be provided in the root\nexport interface CoreOrderConfig {\n automationsOrderWorkflowEnabled$: Observable;\n getMultiPackages(packageIds: string[]): Observable;\n listenToBusiness(\n businessId: string,\n mask: Partial,\n readFilter?: Partial,\n ): Observable;\n listenToBusinessUsers(partnerId: string, businessId: string): Observable;\n}\n\nexport interface PageOrderConfig {\n orderId$: Observable;\n businessId$?: Observable | undefined;\n}\n\nexport interface OrderBusinessConfig {\n getBusinessUrl?(businessId: string): string | Promise;\n}\n\nexport const CORE_ORDER_CONFIG_TOKEN = new InjectionToken(\n '[Orders]: Injection token for global config',\n);\n\nexport const PAGE_ORDER_CONFIG_TOKEN = new InjectionToken(\n '[Orders]: Injection token for a page looking at an individual order',\n);\n\nexport const ORDER_BUSINESS_CONFIG_TOKEN = new InjectionToken(\n '[Orders]: Injection token for business-specific config',\n);\n", "import { Inject, Injectable } from '@angular/core';\nimport { AccountGroup, ProjectionFilter, ReadFilter } from '@galaxy/account-group';\nimport { filterNullAndUndefined } from '@vendasta/rx-utils';\nimport {\n CommonField,\n ConfigInterface,\n CustomField,\n Field,\n NotificationsService,\n Order,\n SalesOrdersService,\n SubscriptionLocation,\n} from '@vendasta/sales-orders';\nimport { User } from '@vendasta/store';\nimport { BehaviorSubject, Observable, combineLatest, of, throwError } from 'rxjs';\nimport {\n catchError,\n distinctUntilChanged,\n filter,\n first,\n map,\n publishReplay,\n refCount,\n shareReplay,\n switchMap,\n switchMapTo,\n tap,\n} from 'rxjs/operators';\nimport { OrderConfirmationActionConfig } from '../components/order-actions/order-actions.component';\nimport { CORE_ORDER_CONFIG_TOKEN, CoreOrderConfig, PAGE_ORDER_CONFIG_TOKEN, PageOrderConfig } from './tokens';\n\nexport interface OrderState {\n subscriptionLocations: Partial>;\n}\n\nconst defaultOrderState: OrderState = {\n subscriptionLocations: {},\n};\n\n/**\n * This is a page service meant to track all the complicated state involved in an order.\n */\n@Injectable()\nexport class OrderStoreService {\n private readonly state$$ = new BehaviorSubject(defaultOrderState);\n\n public readonly order$: Observable;\n public readonly orderConfig$: Observable;\n public readonly orderConfirmationActionConfig$: Observable;\n public readonly productIds$: Observable;\n\n // Push values to this subject to cause the order to be re-fetched.\n private readonly reloadOrder$$ = new BehaviorSubject(undefined);\n private readonly orderId$: Observable;\n private readonly businessId$: Observable;\n public readonly business$: Observable;\n public readonly businessUsers$: Observable;\n\n loadingOrder$$ = new BehaviorSubject(true);\n loadingOrder$ = this.loadingOrder$$.asObservable();\n\n constructor(\n @Inject(CORE_ORDER_CONFIG_TOKEN) public readonly coreOrderConfig: CoreOrderConfig,\n @Inject(PAGE_ORDER_CONFIG_TOKEN) public readonly pageOrderConfig: PageOrderConfig,\n private readonly salesOrdersService: SalesOrdersService,\n protected readonly salesOrdersNotificationService: NotificationsService,\n ) {\n this.orderId$ = this.pageOrderConfig.orderId$.pipe(distinctUntilChanged());\n this.businessId$ = this.pageOrderConfig.businessId$.pipe(distinctUntilChanged());\n\n this.business$ = this.businessId$.pipe(\n switchMap((businessId) => {\n const mask = new ProjectionFilter({ napData: true, accountGroupExternalIdentifiers: true });\n const readFilter = new ReadFilter({ includeDeleted: true });\n return this.coreOrderConfig.listenToBusiness(businessId, mask, readFilter);\n }),\n shareReplay(1),\n );\n\n this.businessUsers$ = this.business$.pipe(\n switchMap((business) => {\n if (!business || business.deleted) {\n return of([]);\n }\n return this.coreOrderConfig.listenToBusinessUsers(\n business.externalIdentifiers.partnerId,\n business.accountGroupId,\n );\n }),\n catchError((err) => {\n if (err.status === 400) {\n return of([]);\n }\n return throwError(err);\n }),\n shareReplay(1),\n );\n\n this.order$ = combineLatest([this.orderId$, this.businessId$, this.reloadOrder$$]).pipe(\n filter(([orderId, businessId]) => Boolean(orderId && businessId)),\n tap(() => {\n this.loadingOrder$$.next(true);\n }),\n switchMap(([orderId, businessId]) => this.salesOrdersService.get(orderId, businessId)),\n tap(() => {\n this.loadingOrder$$.next(false);\n }),\n catchError((err) => {\n // TODO: Add RXJS retry logic - only set loading to false after last retry fails\n this.loadingOrder$$.next(false);\n return throwError(err);\n }),\n shareReplay(1),\n );\n\n this.orderConfig$ = this.order$.pipe(\n filterNullAndUndefined(),\n distinctUntilChanged(),\n switchMap((order) => this.salesOrdersService.getConfig(order.partnerId, order.marketId)),\n shareReplay(1),\n );\n\n this.productIds$ = this.setupProductIds$();\n this.orderConfirmationActionConfig$ = combineLatest([\n this.orderConfig$,\n this.coreOrderConfig.automationsOrderWorkflowEnabled$,\n ]).pipe(\n map(([salesOrdersConfig, automationsOrderWorkflowFlag]) => ({\n allowSendDirectToAdmin: salesOrdersConfig.workflowStepOptions.allowSendDirectToAdmin,\n allowSendToCustomer: salesOrdersConfig.workflowStepOptions.allowSendToCustomer,\n leaseToAutomations: automationsOrderWorkflowFlag,\n customSalespersonActions: salesOrdersConfig.workflowStepOptions.customSalespersonActions,\n })),\n );\n }\n\n // user is implicitly grabbed from the context in the Sales Orders service\n isUserSubscribedToOrderAtLocation(location: SubscriptionLocation): Observable {\n return combineLatest([this.orderId$, this.businessId$]).pipe(\n switchMap(([orderId, businessId]) =>\n this.salesOrdersNotificationService.getSubscribedLocations(businessId, orderId),\n ),\n tap((locations) => {\n const subscriptionLocations: Partial> = (locations || []).reduce(\n (acc, curr) => {\n return { ...acc, [curr]: true };\n },\n {},\n );\n this.state$$.next({ ...this.state$$.getValue(), subscriptionLocations });\n }),\n switchMapTo(this.state$$.pipe(map((state) => state.subscriptionLocations[location] || false))),\n );\n }\n\n // user is implicitly grabbed from the context in the Sales Orders service\n async subscribeUserToOrderAtLocation(location: SubscriptionLocation): Promise {\n await this.salesOrdersNotificationService\n .subscribe(await this.getBusinessId(), await this.getOrderId(), location)\n .toPromise();\n this.setSubscriptionLocation(location, true);\n }\n\n // user is implicitly grabbed from the context in the Sales Orders service\n async unSubscribeUserToOrderAtLocation(location: SubscriptionLocation): Promise {\n await this.salesOrdersNotificationService\n .unsubscribe(await this.getBusinessId(), await this.getOrderId(), location)\n .toPromise();\n this.setSubscriptionLocation(location, false);\n }\n\n async sendExistingOrderToCustomerForApproval(\n userId: string,\n expiry: Date | null,\n orderIsSmbPayable?: boolean,\n ): Promise {\n return this.salesOrdersService.sendExistingOrderToCustomerForApproval(\n await this.getOrderId(),\n await this.getBusinessId(),\n userId,\n expiry,\n orderIsSmbPayable,\n );\n }\n\n async convertToDraft(): Promise {\n return this.salesOrdersService.convertToDraft(await this.getOrderId(), await this.getBusinessId()).toPromise();\n }\n\n async submitDraft({\n customFormData,\n extraFieldsData,\n commonFormData,\n userId,\n expiry,\n }: {\n customFormData: CustomField[];\n extraFieldsData: Field[];\n commonFormData: CommonField[];\n userId: string;\n expiry?: Date;\n }): Promise {\n const leaseToAutomations = await this.getLeasedToAutomations();\n if (leaseToAutomations) {\n return this.salesOrdersService\n .leaseToAutomations(\n await this.getBusinessId(),\n await this.getOrderId(),\n customFormData,\n commonFormData,\n extraFieldsData,\n 'approve',\n )\n .toPromise();\n }\n if (!userId) {\n return this.salesOrdersService\n .submitDraftSalesOrder(\n await this.getOrderId(),\n await this.getBusinessId(),\n customFormData,\n commonFormData,\n extraFieldsData,\n )\n .toPromise();\n }\n return this.salesOrdersService\n .submitDraftForCustomerApproval(\n await this.getOrderId(),\n await this.getBusinessId(),\n userId,\n customFormData,\n commonFormData,\n extraFieldsData,\n expiry,\n )\n .toPromise();\n }\n\n // Request cancellation of an order. If order is not provided, the orderId and\n // accountGroupId from the route will be used.\n async requestCancellation(\n notes: string,\n order?: {\n orderId: string;\n businessId: string;\n },\n ): Promise {\n const orderId = order ? order.orderId : await this.getOrderId();\n const businessId = order ? order.businessId : await this.getBusinessId();\n return this.salesOrdersService.requestCancellation(orderId, businessId, notes).toPromise();\n }\n\n reloadOrder(): void {\n this.reloadOrder$$.next(undefined);\n }\n\n private getOrderId(): Promise {\n return this.orderId$.pipe(filter(Boolean), first()).toPromise();\n }\n\n private getBusinessId(): Promise {\n return this.businessId$.pipe(filter(Boolean), first()).toPromise();\n }\n\n private getPartnerId(): Promise {\n return this.order$\n .pipe(\n map((order) => order.partnerId),\n filter(Boolean),\n first(),\n )\n .toPromise();\n }\n\n private getLeasedToAutomations(): Promise {\n return this.coreOrderConfig.automationsOrderWorkflowEnabled$.pipe(filterNullAndUndefined(), first()).toPromise();\n }\n\n private setSubscriptionLocation(location: SubscriptionLocation, subscribed: boolean): void {\n this.state$$.next({\n ...this.state$$.getValue(),\n subscriptionLocations: {\n ...this.state$$.getValue().subscriptionLocations,\n [location]: subscribed,\n },\n });\n }\n\n private setupProductIds$(): Observable {\n const packages$ = this.order$.pipe(\n switchMap((order) => {\n const packageLineItems = order.lineItems?.filter((li) => !!li.packageId) || [];\n if (packageLineItems.length > 0) {\n return this.coreOrderConfig.getMultiPackages(packageLineItems.map((li) => li.packageId));\n }\n return of([]);\n }),\n shareReplay(1),\n );\n return combineLatest([this.order$, packages$]).pipe(\n map(([order, packages]) => {\n const unpackagedApps = order.lineItems?.filter((li) => !!li.appKey) || [];\n const appIds = unpackagedApps.map((np) => np.appKey.appId);\n packages.map((p) => (p.lineItems.lineItems || []).map((li) => appIds.push(li.id)));\n return appIds;\n }),\n publishReplay(1),\n refCount(),\n );\n }\n}\n", "{\n \"COMMON\": {\n \"ACTION_LABELS\": {\n \"BACK\": \"Back\"\n },\n \"ORDERS\": {\n \"AGREE_TO_PURCHASE\": \"Agree to purchase\",\n \"PURCHASE\": \"Purchase\",\n \"PAYMENT\": \"Payment\",\n \"DECLINE\": \"Decline\",\n \"DRAFT_ORDER\": \"Save as draft\",\n \"NO_LONGER_AVAILABLE\": \"This order can't be altered. For more info, please contact your sales representative.\",\n \"ORDER_FORM_LABEL\": \"Order form\",\n \"PRICING\": \"Pricing\",\n \"PRINT\": \"Print\",\n \"REVIEW\": \"Review order\",\n \"UPDATE_ORDER_FORM_ANSWERS\": \"Update order form answers\",\n \"LOGIN\": \"Log in\",\n \"ERROR\": \"Error\",\n \"ERROR_LOADING_MESSAGE\": \"We were unable to load this order\",\n \"ERROR_AT_LEAST_ONE_LINE_ITEM\": \"Order must have at least 1 line item. Please add an item.\",\n \"SUBMIT_ORDER\": \"Submit order\",\n \"SUBMIT_FOR_CUSTOMER_APPROVAL\": \"Submit for customer approval\",\n \"RESUBMIT_FOR_CUSTOMER_APPROVAL\": \"Resubmit for customer approval\",\n \"CONVERT_TO_DRAFT\": \"Convert to draft\",\n \"CANCEL_ORDER\": \"Cancel order\",\n \"UNSUBSCRIBE_FROM_ORDER\": \"Unsubscribe from update notifications\",\n \"SUBSCRIBE_TO_ORDER\": \"Subscribe to update notifications\",\n \"TRY_AGAIN\": \"Try again\",\n \"TERMS_OF_SERVICE_ERROR\": \"You must agree to the Terms of Service to continue.\",\n \"ORDER_FORM_ERROR\": \"Some of the required fields aren't filled out. Update those and try again.\",\n \"PAYMENT_METHOD_ERROR\": \"A valid payment method is required.\",\n \"ERROR_CUSTOM_PRICE\": \"We could not update the amount. Please try again.\",\n \"SUCCESS_CUSTOM_PRICE\": \"Successfully updated the amount\",\n \"EDIT\": \"Edit\",\n \"ADD\": \"Add\",\n \"CANCEL\": \"Cancel\",\n \"UPLOAD\": \"Upload\",\n \"REQUIRED\": \"Required\",\n \"ERROR_LESS_THAN_ONE\": \"Must be 1 or greater\",\n \"ORDER_SUMMARY\": \"Order Summary\",\n \"INVALID_CARD_ERROR\": \"Invalid credit card details.\",\n \"INVALID_USER_SIGNATURE_ERROR\": \"A signature is required.\"\n },\n \"EDIT_ORDERS\": {\n \"FULFILLMENT_TITLE\": \"Fulfillment orders\",\n \"VIEW_LINK\": \"View\",\n \"DETAILS_NEEDED_BANNER\": \"Details Needed\",\n \"DETAILS_NEEDED_BANNER_DESCRIPTION\": \"Fulfillment form information is required for an item\",\n \"VIEW_FORMS\": \"View forms\"\n },\n \"ORDER_DETAILS\": {\n \"TITLE\": \"Order details\",\n \"COMPANY\": \"Company\",\n \"ADDRESS\": \"Address\",\n \"COMPANY_AND_ADDRESS\": \"Company & Address\",\n \"COMPANY_NAME_AND_ADDRESS\": \"Company name and address\",\n \"CONTRACT_START\": \"Contract Start\",\n \"CONTRACT_DURATION\": \"Contract Duration\",\n \"CONTRACT_START_UPDATE_ERROR\": \"Failed to update contract start date.\",\n \"CONTRACT_DURATION_UPDATE_ERROR\": \"Failed to update contract duration.\",\n \"NO_CONTRACT_DURATION\": \"No contract length\",\n \"LENGTH\": \"Length\",\n \"FREQUENCY\": \"Frequency\",\n \"CUSTOMER_NOTES_TITLE\": \"Customer notes\",\n \"CUSTOMER_NOTES_INFO\": \"These notes will be visible to the customer.\",\n \"ADMINISTRATIVE_NOTES_TITLE\": \"Administrator notes\",\n \"ADMINISTRATIVE_NOTES_INFO\": \"These notes will be hidden from the customer, and only visible to admins and salespeople.\",\n \"EMPTY_NOTES\": \"No notes added\",\n \"NOTES_UPDATE_ERROR\": \"Failed to update notes.\",\n \"ATTACHMENTS\": \"Attachments\",\n \"UPLOAD_ATTACHMENTS\": \"Upload attachments\",\n \"EMPTY_ATTACHMENTS\": \"No files added\",\n \"SUCCESSFUL_UPLOAD\": \"Successfully uploaded {{numberFilesUploaded}} file(s)\",\n \"SUBMITTED_BY\": \"Submitted by:\",\n \"TAGS\": \"Tags\",\n \"EMPTY_TAGS\": \"No tags added\",\n \"NEW_TAG\": \"Add {{newTag}} as a new tag\",\n \"TAGS_UPDATE_ERROR\": \"Your tags couldn't be updated. Please try again.\",\n \"CONTRACT_DURATIONS\": {\n \"DAY\": \"day(s)\",\n \"WEEK\": \"week(s)\",\n \"MONTH\": \"month(s)\",\n \"YEAR\": \"year(s)\"\n }\n },\n \"ORDER_CURRENCY\": {\n \"TITLE\": \"Order Currency\",\n \"DESCRIPTION\": \"Optionally override the currency on the order. You must manually update the prices on the order items to reflect the chosen currency.\",\n \"VMF_ONLY_BADGE\": \"VMF ONLY\",\n \"DEFAULT_OPTION\": \"Default\"\n },\n \"LINE_ITEMS\": {\n \"TITLE\": \"Order Contents\",\n \"ITEM\": \"Item\",\n \"QUANTITY\": \"Quantity\",\n \"UNIT_PRICE\": \"Unit Price\",\n \"DISCOUNT\": \"Discount\",\n \"FREQUENCY\": \"Frequency\",\n \"REMOVE\": \"Remove item\",\n \"ADD\": \"Add items\",\n \"ADD_ROW\": \"Add row\",\n \"REMOVE_ROW\": \"Remove row\",\n \"GREATER_THAN_ZERO\": \"Must be greater than 0\",\n \"EDITION\": \"Edition\",\n \"EDITION_REQUIRED_FOR_PRICE\": \"Edition required to set price\",\n \"QUANTITY_DISABLED\": \"You can only activate one of these at a time\",\n \"REVENUE_REQUIRED\": \"Price is required\",\n \"GREATER_THAN_OR_EQUAL_TO_ZERO\": \"Must be greater than or equal to zero\",\n \"MUST_BE_NUM\": \"Must be a number\",\n \"FREQUENCY_REQUIRED\": \"Frequency is required\"\n },\n \"ACTIVITY\": {\n \"TITLE\": \"Activity\",\n \"CREATED_BY\": \"Created by {{name}}\"\n },\n \"SUMMARY\": {\n \"TITLE\": \"Retail Summary\"\n },\n \"ORDER_SUBMITTED\": {\n \"ORDER_SUBMITTED\": \"Order submitted\",\n \"RECEIVE_CONFIRMATION_SHORTLY\": \"Thank you for your order, it will be processed shortly.\",\n \"ITEM_HAS_FULFILLMENT_FORM\": \"Some items may require additional information from you or your client before they can be fulfilled.\",\n \"VIEW_ACCOUNT_DETAILS\": \"View account details\",\n \"SUCCESS\": \"Success!\",\n \"ORDER_HAS_BEEN_RECEIVED\": \"Your order has been received.\",\n \"FULFILLMENT_FORMS\": \"Fulfillment forms\",\n \"FULFILLMENT_FORMS_SUBTITLE\": \"The forms below will need to get completed before fulfillment can begin\",\n \"BACK_TO_ACCOUNT\": \"Back to account\",\n \"VIEW_ORDER\": \"View order\"\n }\n },\n \"FULFILLMENT\": {\n \"TITLE\": \"Fulfillment\",\n \"SUBTITLE\": \"Some products have Task Manager projects tied to them. These are available at no extra cost.\",\n \"SERVICE_PROVIDER\": \"Service Provider\",\n \"PRODUCTS\": \"Products\",\n \"PROJECT_NAME\": \"Project Name\",\n \"YOU\": \"You\",\n \"LEARN_MORE\": \"Learn more\",\n \"LEARN_MORE_DIALOG\": {\n \"TITLE\": \"Fulfillment projects\",\n \"DESCRIPTION_LINE_1\": \"Your Vendasta subscription includes access to Task Manager. This tool allows you to track and fulfill services on behalf of your clients all in one place.\",\n \"DESCRIPTION_LINE_2\": \"Some of the products in this order have a corresponding Task Manager project available. These projects come in two flavors, depending on who is set to fulfill them\",\n \"SERVICE_PROVIDER_TITLE\": \"You're the service provider\",\n \"SERVICE_PROVIDER_DESCRIPTION\": \"Projects give you guidance on how to fulfill on those services, so even if you aren't familiar with the product, you can assist your customer.\",\n \"NOT_SERVICE_PROVIDER_TITLE\": \"You're not the service provider\",\n \"NOT_SERVICE_PROVIDER_DESCRIPTION\": \"You can track the progress of these projects in Task Manager, allowing you to provide meaningful status updates to your clients.\"\n },\n \"PROJECTS\": \"Projects\",\n \"FULFILLMENT_FORMS\": \"Fulfillment forms\",\n \"SHARE\": \"Share\"\n },\n \"SALES_ORDERS\": {\n \"ORDERS\": \"Orders\"\n }\n}\n", "import { Observable } from 'rxjs';\nimport { map, take } from 'rxjs/operators';\nimport { DatePipe } from '@angular/common';\nimport { Component, Input, OnInit } from '@angular/core';\nimport { OrderDetailAgreement, OrderDetailField } from '@vendasta/sales-ui';\nimport { Status, UserInterface, Order } from '@vendasta/sales-orders';\n\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: 'agreements',\n templateUrl: 'agreements.component.html',\n styleUrls: ['agreements.component.scss', '../history/history.component.scss'],\n})\nexport class AgreementsComponent implements OnInit {\n @Input() order: Order;\n @Input() salesOrderUsers$: Observable<{ [key: string]: UserInterface }>;\n @Input() openExpansionPanel$: Observable;\n\n orderDetails: OrderDetailField[];\n approvalDate: string;\n agreements: OrderDetailAgreement[];\n\n ngOnInit(): void {\n this.initAgreements();\n this.initApprovalDate();\n }\n\n private initAgreements(): void {\n this.salesOrderUsers$\n .pipe(\n map((users) => {\n return OrderDetailAgreement.createFromOrderWithUsers(this.order, users);\n }),\n take(1),\n )\n .subscribe((agreements) => {\n this.agreements = agreements;\n });\n }\n\n private initApprovalDate(): void {\n this.salesOrderUsers$\n .pipe(\n map((users) => {\n return OrderDetailField.createFromOrderWithUsers(this.order, users);\n }),\n take(1),\n )\n .subscribe((orderDetails) => {\n this.orderDetails = orderDetails;\n const customerApproval = this.getLastCustomerApproval();\n if (customerApproval) {\n this.approvalDate = new DatePipe('en-US').transform(customerApproval.date, 'MMMM d, y, h:mm:ss a');\n }\n });\n }\n\n getLastCustomerApproval(): OrderDetailField {\n if (!this.orderDetails) {\n return null;\n }\n // Clone the order details so reverse doesn't break the original array\n const orderDetailsCopy = [...this.orderDetails];\n orderDetailsCopy.reverse();\n // Get the matching history item if at least two entries exist and one is approved by the customer\n return orderDetailsCopy.find(\n (historyItem, index, statusHistory) =>\n index < statusHistory.length - 1 &&\n !historyItem.status &&\n statusHistory[index + 1].status &&\n statusHistory[index + 1].status === Status.SUBMITTED_FOR_CUSTOMER_APPROVAL,\n );\n }\n}\n", " 0\">\n \n \n
\n
\n {{ 'FRONTEND.SALES_UI.LAST_ACCEPTED' | translate }}\n
\n
\n {{ approvalDate }}\n
\n
\n
\n \n \n \n
\n check_circle_outline\n highlight_off\n
\n
\n

\n

\n {{ 'COMMON.ORDER_DETAILS.SUBMITTED_BY' | translate }}\n {{ agreement.name }}\n

\n
\n \n
\n
\n
\n
\n
\n", "import { Component, Inject, ViewChild } from '@angular/core';\nimport { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';\nimport { TranslateService } from '@ngx-translate/core';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { FileInfo, FileUploadError, GalaxyUploaderComponent } from '@vendasta/galaxy/uploader';\nimport { BehaviorSubject } from 'rxjs';\n\n@Component({\n selector: 'orders-attachments-dialog',\n templateUrl: 'attachments-dialog.component.html',\n styleUrls: ['./attachments-dialog.component.scss'],\n})\nexport class AttachmentsDialogComponent {\n numberFilesUploaded = 0;\n maxNumberFiles = 100;\n files: FileInfo[] = [];\n loading$$ = new BehaviorSubject(false);\n\n @ViewChild('glxyUploader') uploader: GalaxyUploaderComponent;\n\n constructor(\n private translate: TranslateService,\n private snack: SnackbarService,\n public dialogRef: MatDialogRef,\n @Inject(MAT_DIALOG_DATA) public data: { uploadUrl: string },\n ) {}\n\n onError(event: FileUploadError): void {\n this.snack.openErrorSnack(event.error.message);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onUpload(event: any): void {\n this.numberFilesUploaded++;\n if (this.files.length === this.numberFilesUploaded) {\n this.loading$$.next(false);\n const successMessage = this.translate.instant('COMMON.ORDER_DETAILS.SUCCESSFUL_UPLOAD', {\n numberFilesUploaded: this.numberFilesUploaded,\n });\n this.snack.openSuccessSnack(successMessage);\n this.dialogRef.close(this.files);\n }\n }\n\n onFilesChanged(files: FileInfo[]): void {\n this.files = files;\n }\n\n onUploadAndClose(): void {\n this.loading$$.next(true);\n this.uploader.uploadQueuedFiles();\n }\n}\n", "

\n {{ 'COMMON.ORDER_DETAILS.UPLOAD_ATTACHMENTS' | translate }}\n

\n\n \n \n\n\n \n \n {{ 'COMMON.ORDERS.UPLOAD' | translate }}\n \n\n", "import { Component, Input, OnDestroy, OnInit } from '@angular/core';\nimport { FileInfo, FileUploadStatus } from '@vendasta/galaxy/uploader';\nimport { MatDialog } from '@angular/material/dialog';\nimport { AttachmentsDialogComponent } from './attachments-dialog.component';\nimport { filter } from 'rxjs/operators';\nimport { SalesOrdersService } from '@vendasta/sales-orders';\nimport { Subscription } from 'rxjs';\n\n@Component({\n selector: 'orders-attachments',\n templateUrl: 'attachments.component.html',\n styleUrls: ['./attachments.component.scss'],\n})\nexport class AttachmentsComponent implements OnInit, OnDestroy {\n @Input() files: FileInfo[];\n @Input() uploadUrl: string;\n @Input() businessId: string;\n @Input() orderId: string;\n @Input() createMode = false;\n @Input() editingDisabled: boolean;\n\n subscriptions: Subscription[] = [];\n\n constructor(public dialog: MatDialog, private salesOrderService: SalesOrdersService) {\n if (!this.files) {\n this.files = [];\n }\n }\n\n ngOnInit(): void {\n // add upload status to previously uploaded files\n // TODO: find out how do we include this status from the parent component.\n if (this.files) {\n this.files.map((file) => {\n return (file.status = FileUploadStatus.Success);\n });\n }\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((sub) => sub.unsubscribe());\n }\n\n updateAttachments(): void {\n if (this.createMode) {\n // skip if component is used in create order\n return;\n }\n const attachments = this.files.map((file) => {\n return {\n name: file.name,\n url: file.url,\n };\n });\n this.subscriptions.push(\n this.salesOrderService.updateAttachments(this.orderId, this.businessId, attachments).subscribe(),\n );\n }\n\n onClickAdd(): void {\n this.subscriptions.push(\n this.dialog\n .open(AttachmentsDialogComponent, {\n width: '720px',\n data: {\n uploadUrl: this.uploadUrl,\n },\n })\n .afterClosed()\n .pipe(filter(Boolean))\n .subscribe((files) => {\n if (!this.files) {\n this.files = [];\n }\n this.files = this.files.concat(files);\n this.updateAttachments();\n }),\n );\n }\n\n getFiles(): FileInfo[] {\n return this.files;\n }\n\n onFileDeleted(file: FileInfo): void {\n const index = this.files.findIndex((f) => f.name === file.name);\n this.files.splice(index, 1);\n this.updateAttachments();\n }\n}\n", "\n \n \n
\n {{ 'COMMON.ORDER_DETAILS.ATTACHMENTS' | translate }}\n
\n
\n \n
\n
\n
\n\n \n
0; else noFiles\">\n \n
\n \n
\n {{ 'COMMON.ORDER_DETAILS.EMPTY_ATTACHMENTS' | translate }}\n
\n
\n
\n
\n", "import { Component, Input, OnDestroy, OnInit, inject } from '@angular/core';\nimport { FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms';\nimport { DateFnsAdapter, MAT_DATE_FNS_FORMATS } from '@angular/material-date-fns-adapter';\nimport { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';\nimport { AccountGroup } from '@galaxy/account-group';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { getLocale } from '@vendasta/galaxy/utility/locale';\nimport { DurationPeriod, SalesOrdersService } from '@vendasta/sales-orders';\nimport { BehaviorSubject, Subscription } from 'rxjs';\nimport { debounceTime, filter, map, scan } from 'rxjs/operators';\nimport { CreateOrderDetailsForm } from './create-order-details.interface';\nimport { Router } from '@angular/router';\nimport { ORDER_BUSINESS_CONFIG_TOKEN } from '../../core/tokens';\n\ninterface periodOption {\n label: string;\n value: number;\n}\n\ninterface address {\n city: string;\n state: string;\n country: string;\n zip: string;\n}\n\ninterface Duration {\n value: number;\n duration: DurationPeriod;\n}\n\nconst DEFAULT_DURATION: Duration = {\n value: 1,\n duration: DurationPeriod.YEAR,\n};\n\n@Component({\n selector: 'orders-create-order-details',\n templateUrl: './create-order-details.component.html',\n styleUrls: ['./create-order-details.component.scss'],\n providers: [\n { provide: MAT_DATE_LOCALE, useValue: getLocale() },\n {\n provide: DateAdapter,\n useClass: DateFnsAdapter,\n deps: [MAT_DATE_LOCALE],\n },\n { provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS },\n ],\n})\nexport class CreateOrderDetailsComponent implements OnInit, OnDestroy {\n readonly minDate = new Date(new Date().setHours(7));\n readonly periodOptions: Array;\n\n private readonly router = inject(Router);\n private readonly orderBusinessConfig = inject(ORDER_BUSINESS_CONFIG_TOKEN, { optional: true });\n\n @Input() business: Pick;\n @Input() orderId: string;\n @Input() contractStartDate = this.minDate;\n @Input() formGroup: UntypedFormGroup;\n @Input() contractDuration: Duration;\n @Input() createMode = false;\n @Input() editingDisabled = false;\n\n protected editingDuration = false;\n readonly contractDurationValueControl = new FormControl(DEFAULT_DURATION.value, [\n Validators.required,\n Validators.min(1),\n ]);\n readonly contractStartControl = new FormControl(this.contractStartDate, [Validators.required]);\n readonly headerForm = new FormGroup({\n contractStartDate: this.contractStartControl,\n contractDuration: new FormGroup({\n value: this.contractDurationValueControl,\n duration: new FormControl(DEFAULT_DURATION.duration, [Validators.required]),\n }),\n });\n\n private readonly updating$$ = new BehaviorSubject(0);\n public readonly isUpdating$ = this.updating$$.pipe(\n scan((acc, value) => acc + value),\n map((total) => total > 0),\n );\n private readonly addUpdateOp = () => this.updating$$.next(1);\n private readonly completeUpdateOp = () => this.updating$$.next(-1);\n\n address: address;\n link$: Promise;\n\n private readonly subscriptions = new Subscription();\n\n constructor(\n private readonly salesOrdersService: SalesOrdersService,\n private readonly snack: SnackbarService,\n ) {\n this.periodOptions = this.initPeriodOptions();\n }\n\n ngOnDestroy(): void {\n this.subscriptions.unsubscribe();\n }\n\n ngOnInit(): void {\n this.initHeaderForm();\n this.address = this.business?.napData;\n this.link$ = this.getAccountLink$(this.business?.accountGroupId);\n }\n\n private initHeaderForm(): void {\n this.formGroup.addControl('headerForm', this.headerForm);\n const durationControl = this.headerForm.controls.contractDuration;\n\n this.contractStartControl.setValue(this.contractStartDate);\n if (this.contractDuration) durationControl.setValue(this.contractDuration);\n\n if (this.editingDisabled === true) this.headerForm.disable();\n if (this.createMode === true) return;\n\n const dueTime = 1000;\n this.subscriptions.add(\n durationControl.valueChanges\n .pipe(\n filter(() => durationControl.valid),\n debounceTime(dueTime),\n )\n .subscribe(() => this.updateContractDuration()),\n );\n\n this.subscriptions.add(\n this.contractStartControl.valueChanges\n .pipe(\n filter(() => this.contractStartControl.valid),\n debounceTime(dueTime),\n )\n .subscribe(() => this.updateContractStartDate()),\n );\n }\n\n // used for editing existing order details (updates contract start date value specifically)\n private updateContractStartDate(): void {\n this.addUpdateOp();\n this.salesOrdersService\n .updateRequestedActivation(this.orderId, this.business.accountGroupId, this.contractStartControl.value)\n .toPromise()\n .catch(() => this.snack.openErrorSnack('COMMON.ORDER_DETAILS.CONTRACT_START_UPDATE_ERROR'))\n .finally(this.completeUpdateOp);\n }\n\n private updateContractDuration(): void {\n this.addUpdateOp();\n this.salesOrdersService\n .updateContractDuration(this.orderId, this.business.accountGroupId, this.headerForm.value.contractDuration)\n .toPromise()\n .catch(() => this.snack.openErrorSnack('COMMON.ORDER_DETAILS.CONTRACT_DURATION_UPDATE_ERROR'))\n .finally(this.completeUpdateOp);\n }\n\n private initPeriodOptions(): periodOption[] {\n return [\n {\n label: 'COMMON.ORDER_DETAILS.CONTRACT_DURATIONS.DAY',\n value: DurationPeriod.DAY,\n },\n {\n label: 'COMMON.ORDER_DETAILS.CONTRACT_DURATIONS.WEEK',\n value: DurationPeriod.WEEK,\n },\n {\n label: 'COMMON.ORDER_DETAILS.CONTRACT_DURATIONS.MONTH',\n value: DurationPeriod.MONTH,\n },\n {\n label: 'COMMON.ORDER_DETAILS.CONTRACT_DURATIONS.YEAR',\n value: DurationPeriod.YEAR,\n },\n ];\n }\n\n public getFormData(): CreateOrderDetailsForm {\n const data = this.headerForm.value;\n data.contractStartDate.setHours(7);\n return data as CreateOrderDetailsForm;\n }\n\n protected showEditableDuration(): void {\n const durationControl = this.headerForm.controls.contractDuration;\n if (!this.contractDuration) {\n durationControl.setValue(DEFAULT_DURATION);\n }\n\n this.editingDuration = true;\n }\n\n protected navigateTo(url: string): void {\n this.router.navigateByUrl(url);\n }\n\n private getAccountLink$(accountGroupId: string): Promise {\n const url = this.orderBusinessConfig?.getBusinessUrl?.(accountGroupId) ?? `/info/${accountGroupId}`;\n return this.makePromise(url);\n }\n\n private makePromise(value: T | Promise): Promise {\n if (value instanceof Promise) {\n return value;\n }\n return Promise.resolve(value);\n }\n}\n", "\n \n \n {{ 'COMMON.ORDER_DETAILS.TITLE' | translate }}\n
\n \n
\n
\n
\n \n
\n \n
\n \n {{ 'COMMON.ORDER_DETAILS.COMPANY_AND_ADDRESS' | translate }}\n \n \n @if (link$ | async; as link) {\n \n {{ business?.napData?.companyName }}\n \n } @else {\n {{ business?.napData?.companyName }}\n }\n \n \n \n
\n {{ address | glxyAddress }}\n
\n
\n
\n
\n\n \n
\n \n {{ 'COMMON.ORDER_DETAILS.CONTRACT_START' | translate }}\n \n \n \n \n \n \n {{ 'COMMON.ORDERS.REQUIRED' | translate }}\n \n \n
\n\n \n
\n \n {{ 'COMMON.ORDER_DETAILS.CONTRACT_DURATION' | translate }}\n \n \n \n \n \n \n {{ 'COMMON.ORDERS.ERROR_LESS_THAN_ONE' | translate }}\n \n \n \n \n \n {{ option.label | translate }}\n \n \n \n \n\n \n {{ 'COMMON.ORDER_DETAILS.NO_CONTRACT_DURATION' | translate }}\n \n \n \n
\n
\n
\n
\n", "// Used in cases where no revenue period is set on the item\n// Examples: Contact Sales and Free items\nimport { RevenueComponent, RevenuePeriod } from '@vendasta/sales-orders';\nimport { UILineItem } from '@vendasta/store';\n\nexport const defaultRevenuePeriodMonthly = RevenuePeriod.MONTHLY;\nexport const defaultCurrencyCodeUSD = 'USD';\n\n// INVALID_ITEM handles scenarios where order items are no longer available.\n// Examples: Has been disabled from the marketplace by the partner or no longer whitelisted to the partner\nexport const INVALID_ITEM: UILineItem = {\n name: 'Invalid Item',\n iconUrl: null,\n lineItem: null,\n};\n\n// defaultRevenue handles scenarios where there's no pricing info on the item (package or product)\n// Examples: Contact Sales and Free items\nexport const defaultRevenue: RevenueComponent = new RevenueComponent({\n value: 0,\n period: defaultRevenuePeriodMonthly,\n isStartingRevenue: false,\n});\n\nexport const PARTNER_ID = 'PARTNER_ID';\nexport const MARKET_ID = 'MARKET_ID';\n\n// Used for providing different styles/templates components for desktop and mobile views\nexport const mobileScreenSizeMax = 600;\n\n// Used for SSC attachments\nexport const DEMO_SSC_UPLOAD_URL = 'https://sales-orders-demo.apigateway.co/file/upload';\nexport const PROD_SSC_UPLOAD_URL = 'https://sales-orders-prod.apigateway.co/file/upload';\n", "import { Currency as PackageCurrency, Frequency, Package, Pricing } from '@vendasta/marketplace-packages';\nimport { UILineItem } from '@vendasta/store';\nimport { LineItem, Revenue, RevenueComponent, RevenuePeriod } from '@vendasta/sales-orders';\nimport { UILineItemWithEditionPricing } from './interface';\nimport { defaultRevenue, defaultRevenuePeriodMonthly } from './constants';\nimport { App } from '@galaxy/marketplace-apps';\nimport { AppWithRetailPricing } from '../components/line-items/line-items.interface';\n\nexport function packageToUILineItem(pkg: Package, currency: string): UILineItem {\n return {\n lineItem: packageToSalesOrderLineItem(pkg, currency),\n name: pkg.name,\n iconUrl: pkg.icon,\n bannerUrl: pkg.headerImageUrl,\n isMultiActivatable: true,\n storeLineItem: packageToSalesOrderLineItem(pkg, currency),\n discountPercentage: 0,\n description: pkg.tagline,\n };\n}\n\nexport function packageToSalesOrderLineItem(pkg: Package, currency: string): LineItem {\n return new LineItem({\n packageId: pkg.packageId,\n currencyCode: !!pkg.pricing && !!pkg.pricing.currency ? PackageCurrency[pkg.pricing.currency] : currency,\n currentRevenue: new Revenue({ revenueComponents: buildPackageRevenueComponentsList(pkg.pricing) }),\n quantity: 1,\n });\n}\n\n// Pricing input is specific to Package type\n// The pricing prices list may contain only a single object with just the startingAt property set to true in the case of contact sales and free packages\nexport function buildPackageRevenueComponentsList(pricing: Pricing): RevenueComponent[] {\n const prices = !!pricing && !!pricing.prices ? pricing.prices : [];\n if (prices.length === 0) {\n return [defaultRevenue];\n }\n return prices.map((p) => {\n return new RevenueComponent({\n value: p.price && p.price > -1 ? p.price : 0,\n period: packageFrequencyToSalesOrderPeriod(p.frequency),\n isStartingRevenue: p.isStartingPrice,\n });\n });\n}\n\nexport function packageFrequencyToSalesOrderPeriod(f: Frequency): RevenuePeriod {\n switch (f) {\n case Frequency.DAILY:\n return RevenuePeriod.DAILY;\n case Frequency.MONTHLY:\n return RevenuePeriod.MONTHLY;\n case Frequency.ONCE:\n return RevenuePeriod.ONETIME;\n case Frequency.WEEKLY:\n return RevenuePeriod.WEEKLY;\n case Frequency.YEARLY:\n return RevenuePeriod.YEARLY;\n default:\n return defaultRevenuePeriodMonthly;\n }\n}\n\nexport function convertSelectedAppToLineItem(\n app: App | AppWithRetailPricing,\n defaultDisplayCurrency: string,\n): UILineItem {\n const uiLineItem: UILineItemWithEditionPricing = {\n lineItem: appToSalesOrderLineItem(app, defaultDisplayCurrency),\n name: app.sharedMarketingInformation.name,\n editionName: app.sharedMarketingInformation.editionName,\n iconUrl: app.sharedMarketingInformation.iconUrl,\n bannerUrl: app.sharedMarketingInformation.bannerImageUrl,\n storeLineItem: appToSalesOrderLineItem(app, defaultDisplayCurrency),\n discountPercentage: 0,\n description: app.sharedMarketingInformation.tagline,\n appType: app.appType,\n };\n\n if (app.price) {\n uiLineItem.usesCustomPricing = app.price.usesCustomPricing;\n }\n\n if (app.activationInformation) {\n uiLineItem.isMultiActivatable = app.activationInformation.multipleActivationsEnabled;\n }\n\n if ('retailPricing' in app) {\n // override the default revenue component if retail pricing exists\n if (app.retailPricing.revenue.revenueComponents.length > 0) {\n uiLineItem.lineItem.currentRevenue = app.retailPricing.revenue;\n uiLineItem.lineItem.currencyCode = app.retailPricing.currencyCode;\n uiLineItem.storeLineItem.currentRevenue = app.retailPricing.revenue;\n uiLineItem.storeLineItem.currencyCode = app.retailPricing.currencyCode;\n }\n }\n\n if (app.parentRequirements?.enabled) {\n uiLineItem.parentIconUrl = app.parentRequirements.parentDetails.iconUrl;\n uiLineItem.parentName = app.parentRequirements.parentDetails.name;\n uiLineItem.parentAppId = app.parentRequirements.parentDetails.key.appId;\n }\n return uiLineItem;\n}\n\nexport function appToSalesOrderLineItem(app: App, currency: string): LineItem {\n return new LineItem({\n appKey: app.key,\n currencyCode: currency,\n currentRevenue: new Revenue({ revenueComponents: [defaultRevenue] }),\n quantity: 1,\n });\n}\n\nexport function calculateDiscountPercentage(uiLineItem: UILineItem): number {\n const storeRevenueComponents = uiLineItem.storeLineItem?.currentRevenue?.revenueComponents;\n if (storeRevenueComponents === null) {\n return 0;\n }\n\n const storeYearlyTotalEstimate = storeRevenueComponents.reduce((total, storeRevenueComponent) => {\n return total + getYearlyTotal(storeRevenueComponent);\n }, 0);\n if (storeYearlyTotalEstimate <= 0) {\n return 0;\n }\n\n const orderRevenueComponents = uiLineItem.lineItem?.currentRevenue?.revenueComponents;\n if (orderRevenueComponents === null) {\n return 0;\n }\n const orderYearlyTotalEstimate = orderRevenueComponents.reduce((total, orderRevenueComponent) => {\n return total + getYearlyTotal(orderRevenueComponent);\n }, 0);\n if (orderYearlyTotalEstimate <= 0) {\n return 0;\n }\n\n const discountPercentage = ((storeYearlyTotalEstimate - orderYearlyTotalEstimate) * 100) / storeYearlyTotalEstimate;\n if (discountPercentage > 100 || discountPercentage < 0) {\n return 0;\n }\n if (0 < discountPercentage && discountPercentage < 1) {\n return 1;\n }\n return Math.floor(discountPercentage);\n}\n\nfunction getYearlyTotal(revenueComponent: RevenueComponent): number {\n switch (revenueComponent.period) {\n case RevenuePeriod.YEARLY:\n return revenueComponent.value;\n case RevenuePeriod.MONTHLY:\n return revenueComponent.value * 12;\n case RevenuePeriod.BIWEEKLY:\n return revenueComponent.value * 26;\n case RevenuePeriod.WEEKLY:\n return revenueComponent.value * 52;\n case RevenuePeriod.DAILY:\n return revenueComponent.value * 365;\n default:\n return revenueComponent.value;\n }\n}\n", "import { CalculateTaxResponse } from '@galaxy/billing';\nimport { TaxOption } from '@vendasta/store';\nimport { LineItem } from '@vendasta/sales-orders';\nimport { Package } from '@vendasta/marketplace-packages';\nimport { App } from '@galaxy/marketplace-apps';\nimport { convertSelectedAppToLineItem, packageToUILineItem } from './shared/utils';\nimport { defaultCurrencyCodeUSD, INVALID_ITEM } from './shared/constants';\nimport { UILineItemWithEditionPricing } from './shared/interface';\n\n// TODO: this needs to fit both PC and SSC (likely as an input from those clients once SSC starts using the lib components)\nexport const ORDER_FORM_FILE_UPLOAD_URL = '/_ajax/v1/marketplace/file/upload';\n\nexport function calculateTaxOptionsFromTaxResponse(response: CalculateTaxResponse): TaxOption[] {\n return response.taxRates.map((item) => {\n return {\n label: item.name,\n percentageMultiplier: item.percentage / 100,\n };\n });\n}\n\nexport function buildOrderItems(\n lineItems: LineItem[],\n packages: Package[] | any[],\n appsAndAddons: App[] | any[],\n defaultCurrency: string,\n): UILineItemWithEditionPricing[] {\n return lineItems.map((li) => {\n let uiLineItem = INVALID_ITEM;\n if (li.packageId) {\n const pack = packages.find((p) => p.packageId === li.packageId);\n uiLineItem = packageToUILineItem(pack, defaultCurrency);\n } else {\n const app = appsAndAddons.find(\n (a) => a.key.appId === li.appKey.appId && (a.key.editionId || '') === (li.appKey.editionId || ''),\n );\n if (app) {\n uiLineItem = convertSelectedAppToLineItem(app, defaultCurrencyCodeUSD);\n }\n }\n uiLineItem.lineItem = li;\n return uiLineItem;\n });\n}\n", "import { Injectable } from '@angular/core';\nimport {\n CommonFormData,\n CustomFieldsAnswer,\n CustomFieldsAnswers,\n IncludedCommonFormFieldsInterface,\n OrderFormFieldOptionsType,\n OrderFormInterface,\n OrderFormOptionsInterface,\n ProductInfoInterface,\n TextBoxFieldOptions,\n} from '@vendasta/store';\nimport { Observable, of, zip, combineLatest, ReplaySubject } from 'rxjs';\nimport {\n GetMultiOrderFormsResponseOrderFormContainerInterface as MPOrderFormContainerInterface,\n MarketplaceAppsApiService,\n} from '@galaxy/marketplace-apps/v1';\nimport { App, AppKey, AppPartnerService } from '@galaxy/marketplace-apps';\nimport {\n CommonField,\n CustomField,\n Field,\n LineItem,\n LineItemInterface,\n SalesOrdersService,\n Order,\n ConfigInterface,\n} from '@vendasta/sales-orders';\nimport { Package, PackageService } from '@vendasta/marketplace-packages';\nimport { filter, map, publishReplay, refCount, switchMap } from 'rxjs/operators';\nimport { TranslateService } from '@ngx-translate/core';\nimport { AccountGroup, AccountGroupService, ProjectionFilter } from '@galaxy/account-group';\nimport { AppType } from '@vendasta/marketplace-apps';\n\n@Injectable()\nexport class OrderFormService {\n private mpOrderForms$: Observable;\n private appQuantityMap$: Observable>;\n\n private order$$: ReplaySubject = new ReplaySubject(1);\n order$: Observable = this.order$$.asObservable();\n private accountGroup$: Observable;\n\n private orderFormOptions$$: ReplaySubject = new ReplaySubject(\n 1,\n );\n orderFormOptions$: Observable = this.orderFormOptions$$.asObservable();\n\n private orderConfig$$: ReplaySubject = new ReplaySubject(1);\n orderConfig$: Observable = this.orderConfig$$.asObservable();\n\n private lineItems$$: ReplaySubject = new ReplaySubject(1);\n lineItems$: Observable = this.lineItems$$.asObservable();\n packageIds$: Observable;\n packages$: Observable;\n apps$: Observable;\n standaloneAppKeys$: Observable;\n includedAppKeys$: Observable;\n uniqueApps$: Observable;\n\n private orderForms$: Observable;\n private commonData$: Observable;\n private productInfo$: Observable;\n private answers$: Observable;\n private extraQuestions$: Observable;\n private extraAnswers$: Observable;\n\n constructor(\n private translateService: TranslateService,\n private appsApiService: MarketplaceAppsApiService,\n private salesOrderSdk: SalesOrdersService,\n private packageService: PackageService,\n private appsService: AppPartnerService,\n private accountGroupService: AccountGroupService,\n ) {}\n\n initialize(\n order: Order,\n orderFormOptions: OrderFormOptionsInterface,\n orderLineItems: LineItemInterface[],\n orderConfig: ConfigInterface,\n ): void {\n this.order$$.next(order);\n this.orderFormOptions$$.next(orderFormOptions);\n this.orderConfig$$.next(orderConfig);\n this.lineItems$$.next(orderLineItems);\n this.refreshOrderForms();\n }\n\n private refreshOrderForms(): void {\n this.lineItems$ = combineLatest([this.order$, this.lineItems$]).pipe(\n map(([order, orderLineItems]) => {\n return order?.lineItems || orderLineItems;\n }),\n );\n\n this.packageIds$ = this.lineItems$.pipe(\n map((lineItems) => {\n return lineItems.map((lineItem) => lineItem.packageId).filter((packageId) => !!packageId);\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.standaloneAppKeys$ = this.lineItems$.pipe(\n map((lineItems) => {\n return lineItems\n .filter((lineItem) => !!lineItem.appKey)\n .map((lineItem) => {\n return new AppKey(lineItem.appKey);\n });\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.packages$ = this.packageIds$.pipe(\n switchMap((packageIds) => {\n return packageIds.length > 0 ? this.packageService.getMulti(packageIds) : of([]);\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.includedAppKeys$ = zip(this.packages$, this.standaloneAppKeys$).pipe(\n map(([packages, standaloneAppKeys]) => {\n const packageAppKeys = packages\n .map((pkg) => {\n const lineItems = pkg && pkg.lineItems && pkg.lineItems.lineItems ? pkg.lineItems.lineItems : [];\n return lineItems.map(\n (item) =>\n new AppKey({\n appId: item.id || '',\n editionId: item.editionId || '',\n }),\n );\n })\n .reduce((a, b) => [...a, ...b], []);\n return standaloneAppKeys.concat(packageAppKeys);\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.apps$ = combineLatest([this.includedAppKeys$, this.order$]).pipe(\n switchMap(([includedAppKeys, order]) =>\n this.appsService.getMulti(includedAppKeys, order.partnerId, order.marketId, null, true),\n ),\n publishReplay(1),\n refCount(),\n );\n\n this.uniqueApps$ = this.apps$.pipe(\n filter((apps) => !!apps && apps.length > 0),\n map((appKeys) => {\n return appKeys.reduce((unique, app) => {\n const { key } = app;\n if (\n unique.find(({ key: uniqueKey }) => uniqueKey.appId === key.appId && uniqueKey.editionId === key.editionId)\n ) {\n return unique;\n }\n\n unique.push(app);\n return unique;\n }, []);\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.mpOrderForms$ = combineLatest([this.apps$, this.order$]).pipe(\n switchMap(([apps, order]) => {\n return this.getMarketplaceOrderForms(order.partnerId, apps);\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.appQuantityMap$ = zip(this.packages$, this.lineItems$).pipe(\n map(([packages, salesLineItems]) => {\n const quantityMap: Map = new Map();\n const allPkgLineItems = packages.reduce((items, pkg) => {\n const pkgLineItem = salesLineItems.find((lineItem) => lineItem.packageId === pkg.packageId);\n const pkgCount = (pkgLineItem ? pkgLineItem.quantity : 1) || 1;\n return items.concat(this.salesOrderLineItemsFromPackage(pkg, pkgCount));\n }, []);\n salesLineItems = salesLineItems.filter((lineItem) => !lineItem.packageId);\n salesLineItems = salesLineItems.concat(allPkgLineItems);\n salesLineItems.forEach((item) => {\n const count = quantityMap.get(item.appKey.appId) || 0;\n quantityMap.set(item.appKey.appId, count + (item.quantity || 1));\n });\n return quantityMap;\n }),\n publishReplay(1),\n refCount(),\n );\n\n this.orderForms$ = combineLatest([\n this.mpOrderForms$,\n this.appQuantityMap$,\n this.uniqueApps$,\n this.orderFormOptions$,\n ]).pipe(\n map(([orderForms, appQuantityMap, apps, options]) => {\n return apps.reduce((forms, app) => {\n let commonFormRequiredFields: IncludedCommonFormFieldsInterface = {};\n let customFields: OrderFormFieldOptionsType[] = [];\n\n if (app.activationInformation.activationSpecificUrlEnabled) {\n customFields.unshift({\n id: 'custom-entry-url-' + app.key.appId,\n label: this.translateService.instant('FRONTEND.SALES_UI.ORDER_FORM.CUSTOM_ENTRY_URL'),\n type: 'text',\n required: options.showOfficeUseQuestions,\n hidden: !options.showOfficeUseQuestions,\n regexValidator: '^(http://|https://).*',\n regexErrorMessage: this.translateService.instant(\n 'FRONTEND.SALES_UI.ORDER_FORM.CUSTOM_ENTRY_URL_REGEX_ERROR',\n ),\n textboxType: 'text',\n } as TextBoxFieldOptions);\n }\n\n const orderForm = orderForms.find((o) => o.appId === app.key.appId);\n\n if (!!orderForm || app.activationInformation.activationSpecificUrlEnabled) {\n if (!!orderForm && orderForm.orderForm && app.activationInformation.orderFormEnabled) {\n if (orderForm.orderForm.commonFormRequiredFields) {\n commonFormRequiredFields = orderForm.orderForm.commonFormRequiredFields;\n }\n if (orderForm.orderForm.orderForm) {\n customFields = customFields.concat(\n orderForm.orderForm.orderForm.map(\n (f) => ({ ...f, options: f.optionsWithLabels } as OrderFormFieldOptionsType),\n ),\n );\n }\n }\n\n let appId = app.key.appId;\n let addonId = '';\n if (app.parentRequirements.enabled && app.appType === AppType.APP_TYPE_ADDON) {\n appId = app.parentRequirements.parentDetails.key.appId;\n addonId = app.key.appId;\n }\n const footerText = orderForm?.orderForm?.footerText ? orderForm.orderForm.footerText : null;\n const createdForm: OrderFormInterface = {\n appId: appId,\n addonId: addonId,\n customFields: customFields,\n commonFormRequiredFields: commonFormRequiredFields,\n };\n if (footerText) {\n createdForm.footerText = footerText;\n }\n forms.push(createdForm);\n\n if (\n !!addonId &&\n app.activationInformation.orderFormEnabled &&\n app.activationInformation.multipleActivationsEnabled &&\n app.activationInformation.separateOrderForms\n ) {\n const count = appQuantityMap.get(app.key.appId) || 1;\n for (let i = 1; i < count; i++) {\n forms.push(createdForm);\n }\n } else if (\n !addonId &&\n app.activationInformation.orderFormEnabled &&\n app.activationInformation.multipleActivationsEnabled\n ) {\n const count = appQuantityMap.get(app.key.appId) || 1;\n for (let i = 1; i < count; i++) {\n forms.push(createdForm);\n }\n }\n }\n\n return forms;\n }, []);\n }),\n );\n\n this.accountGroup$ = this.order$.pipe(\n switchMap((o) => this.accountGroupService.get(o.businessId, new ProjectionFilter({ napData: true }))),\n );\n this.commonData$ = combineLatest([this.order$, this.accountGroup$]).pipe(\n switchMap(([order, accountGroup]) => {\n const commonData: CommonFormData = OrderFormService.populateCommonFormData(accountGroup);\n // commonFields from the order take precedence over the auto-populated data\n if (order) {\n (order.commonFields || []).map((cf) => {\n let fieldAnswer = cf.field.answer;\n try {\n fieldAnswer = JSON.parse(fieldAnswer);\n } catch (error) {\n // do nothing, this code should be cleaned up.\n }\n const fieldId = cf.field.fieldId;\n commonData[fieldId] = fieldAnswer;\n });\n }\n return of(commonData);\n }),\n );\n\n this.productInfo$ = this.uniqueApps$.pipe(\n map((apps) => {\n const productInfoMap: Map = new Map();\n apps.map((app) => {\n let productInfo: ProductInfoInterface = {\n productId: 'temporary',\n addonInfo: [],\n };\n if (app.parentRequirements.enabled && app.appType === AppType.APP_TYPE_ADDON) {\n if (productInfoMap.has(app.parentRequirements.parentDetails.key.appId)) {\n productInfo = productInfoMap.get(app.parentRequirements.parentDetails.key.appId);\n }\n productInfo.name = app.parentRequirements.parentDetails.name;\n productInfo.tagline = app.parentRequirements.parentDetails.tagline;\n productInfo.icon = app.parentRequirements.parentDetails.iconUrl;\n productInfo.addonInfo.push({\n addonId: app.key.appId,\n name: app.sharedMarketingInformation.name,\n });\n productInfo.productId = app.parentRequirements.parentDetails.key.appId;\n productInfoMap.set(productInfo.productId, productInfo);\n } else {\n if (productInfoMap.has(app.key.appId)) {\n productInfo = productInfoMap.get(app.key.appId);\n }\n productInfo.name = app.sharedMarketingInformation.name;\n productInfo.tagline = app.sharedMarketingInformation.tagline;\n productInfo.icon = app.sharedMarketingInformation.iconUrl;\n productInfo.productId = app.key.appId;\n productInfoMap.set(productInfo.productId, productInfo);\n }\n });\n return Array.from(productInfoMap.values());\n }),\n );\n\n this.answers$ = this.order$.pipe(\n map((order) => {\n if (order) {\n const answers: CustomFieldsAnswers[] = [];\n if (order.customFields) {\n order.customFields.map((c) => {\n let fieldAnswers: CustomFieldsAnswer[] = [];\n if (c.fields) {\n fieldAnswers = c.fields.map((f) => ({ fieldId: f.fieldId, answer: f.answer }));\n }\n answers.push({\n productID: c.productId || (c.addonKey ? c.addonKey.appId : ''),\n addonKey: c.addonKey,\n customFieldsAnswers: fieldAnswers,\n });\n });\n }\n return answers;\n }\n return [];\n }),\n );\n\n this.extraQuestions$ = this.orderConfig$.pipe(\n map((config) => {\n return !!config && !!config.extraFields\n ? config.extraFields.map((f) => ({ ...f, options: f.optionsWithLabels } as OrderFormFieldOptionsType))\n : [];\n }),\n );\n\n this.extraAnswers$ = this.order$.pipe(\n map((order) => {\n if (order) {\n return order.extraFields || [];\n }\n return [];\n }),\n );\n }\n\n private salesOrderLineItemsFromPackage(pkg: Package, pkgCount: number): LineItem[] {\n if (!pkg.lineItems || !pkg.lineItems.lineItems || !pkg.lineItems.lineItems.length) {\n return [];\n }\n return pkg.lineItems.lineItems.map(\n (lineItem) =>\n new LineItem({\n appKey: { appId: lineItem.id, editionId: lineItem.editionId },\n quantity: (lineItem.quantity || 1) * pkgCount,\n }),\n );\n }\n\n private getMarketplaceOrderForms(partnerId: string, apps: App[]): Observable {\n if (apps?.length > 0) {\n const appIds: string[] = apps\n .filter((app) => app?.activationInformation?.orderFormEnabled)\n .map((app) => app?.key?.appId);\n if (appIds?.length > 0) {\n return this.appsApiService\n .getMultiOrderForms({ partnerId: partnerId, appIds: appIds })\n .pipe(map((resp) => resp.orderFormContainer || []));\n }\n }\n return of([]);\n }\n\n get getOrderForms(): Observable {\n return this.orderForms$;\n }\n\n get getCommonData(): Observable {\n return this.commonData$;\n }\n\n get getProductInfo(): Observable {\n return this.productInfo$;\n }\n\n get getAnswers(): Observable {\n return this.answers$;\n }\n\n get getExtraQuestions(): Observable {\n return this.extraQuestions$;\n }\n\n get getExtraAnswers(): Observable {\n return this.extraAnswers$;\n }\n\n static populateCommonFormData(accountGroup?: AccountGroup): CommonFormData {\n const napData = accountGroup?.napData;\n const fullAddress = [napData?.address, napData?.city, napData?.state, napData?.zip].filter(Boolean).join(', ');\n return {\n business_account_group_id: accountGroup?.accountGroupId ?? '',\n business_name: napData?.companyName ?? '',\n business_address: fullAddress,\n business_phone_number: napData?.workNumber?.filter(Boolean)[0] ?? '',\n business_website: napData?.website ?? '',\n };\n }\n\n updateAnswers(commonFormData: CommonField[], customFormData: CustomField[], extraFields: Field[]): Observable {\n return this.order$.pipe(\n switchMap((order) => {\n return this.salesOrderSdk.updateAnswers(\n order.orderId,\n order.businessId,\n customFormData,\n commonFormData,\n extraFields,\n );\n }),\n );\n }\n\n refresh(order: Order): void {\n this.order$$.next(order);\n }\n}\n", "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n Input,\n OnDestroy,\n OnInit,\n Output,\n ViewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { UntypedFormControl, UntypedFormGroup } from '@angular/forms';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport {\n CommonField,\n ConfigInterface,\n CustomField,\n Field,\n LineItemInterface,\n Order,\n Status,\n UserInterface as User,\n} from '@vendasta/sales-orders';\nimport {\n CommonFormData,\n CustomFieldsAnswer,\n CustomFieldsAnswers,\n OrderFormFieldOptionsType,\n OrderFormInterface,\n OrderFormOptionsInterface,\n ProductInfoInterface,\n OrderFormComponent as StoreOrderFormComponent,\n} from '@vendasta/store';\nimport { BehaviorSubject, Observable, Subject, Subscription, combineLatest, of } from 'rxjs';\nimport { debounceTime, map, take, tap } from 'rxjs/operators';\nimport { OrderFormService } from './order-form.service';\n\ninterface OrderData {\n orderForms: OrderFormInterface[];\n productInfo: ProductInfoInterface[];\n commonData: CommonFormData;\n answers: CustomFieldsAnswers[];\n extraQuestions: OrderFormFieldOptionsType[];\n extraAnswers: CustomFieldsAnswer[];\n users: User[];\n orderFormOptions: OrderFormOptionsInterface;\n}\n\n@Component({\n templateUrl: 'order-form.component.html',\n providers: [OrderFormService],\n selector: 'orders-form',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n styleUrls: ['../../shared/shared-styles.scss', './order-form.component.scss'],\n})\nexport class OrderFormComponent implements OnInit, OnDestroy {\n @ViewChild('vaOrderForm')\n public orderFormComponent: StoreOrderFormComponent;\n\n // The order and its configuration\n @Input() order: Order;\n @Input() orderFormOptions: OrderFormOptionsInterface;\n @Input() orderConfig: ConfigInterface;\n\n // Optionally used within the create order experience\n @Input() lineItems: LineItemInterface[];\n\n // Required attributes\n @Input() userOptions: User[];\n @Input() fileUploadUrl = '';\n @Input() showSelfSaveButton = false;\n @Input() answers: CustomFieldsAnswers[] = [];\n @Input() parentForm: UntypedFormGroup;\n\n // External identifiers\n @Input() partnerId: string;\n @Input() businessId: string;\n\n @Output() loading: EventEmitter = new EventEmitter();\n\n answers$: Observable;\n orderData$: Observable;\n startOpen$: Observable;\n submitting = false;\n orderStatus: Status;\n\n loading$$: BehaviorSubject = new BehaviorSubject(true);\n loading$: Observable = this.loading$$.asObservable();\n\n subscriptions: Subscription[] = [];\n\n constructor(\n private alertService: SnackbarService,\n private salesOrderFormService: OrderFormService,\n private cdr: ChangeDetectorRef,\n ) {}\n\n ngOnInit(): void {\n this.salesOrderFormService.initialize(this.order, this.orderFormOptions, this.lineItems, this.orderConfig);\n\n this.answers$ = this.salesOrderFormService.getAnswers.pipe(\n map((answers) => {\n if (answers.length > 0) {\n return answers;\n }\n\n return this.answers;\n }),\n );\n\n this.orderData$ = combineLatest([\n this.salesOrderFormService.getOrderForms,\n this.salesOrderFormService.getProductInfo,\n this.salesOrderFormService.getCommonData,\n this.answers$,\n this.salesOrderFormService.getExtraAnswers,\n this.salesOrderFormService.getExtraQuestions,\n ]).pipe(\n // OrderFormService emits in a burst when the order re-renders which caused order form issues, debounce helps prevent that (https://vendasta.jira.com/browse/QOF-1472)\n debounceTime(250),\n map(([orderForms, productInfo, commonData, answers, extraAnswers, extraQuestions]) => {\n return {\n orderForms: orderForms,\n productInfo: productInfo,\n commonData: commonData,\n answers: answers,\n extraQuestions: extraQuestions,\n extraAnswers: extraAnswers,\n users: this.userOptions,\n orderFormOptions: this.orderFormOptions,\n };\n }),\n tap(() => this.loading$$.next(false)),\n );\n\n this.startOpen$ = this.salesOrderFormService.order$.pipe(\n take(1),\n map(\n (o) =>\n !o ||\n (o.status !== Status.PROCESSING &&\n o.status !== Status.ACTIVATION_ERRORS &&\n o.status !== Status.FULFILLED &&\n o.status !== Status.ARCHIVED),\n ),\n );\n\n this.subscriptions.push(this.loading$.pipe(tap((loading) => this.loading.emit(loading))).subscribe());\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((sub) => {\n sub.unsubscribe();\n });\n }\n\n /**\n * returns the data from common form fields in a format that is usable by sales orders\n */\n public getCommonFormData(): CommonField[] {\n return this.orderFormComponent.getCommonFormData();\n }\n\n /**\n * returns the data from custom forms in a format that is usable by sales orders\n */\n public getCustomFormData(): CustomField[] {\n return this.orderFormComponent.getCustomFormData();\n }\n\n /**\n * returns the data from extra fields in a format that is usable by sales orders\n */\n public getExtraFieldsData(): Field[] {\n return this.orderFormComponent.getExtraFieldsData();\n }\n\n /**\n * returns true if the order form contains fields that need to be filled out by the partner\n */\n public requiresPartnerInput(): boolean {\n return this.orderFormComponent.orderFormRequiresPartnerInput();\n }\n\n /**\n * changes the read only state of the sales order form\n */\n public changeReadOnly(readOnly: boolean): void {\n this.orderFormComponent.changeReadOnly(readOnly);\n }\n\n private setAsTouched(form: UntypedFormGroup): void {\n form.markAsTouched();\n const controls = form.controls;\n for (const i of Object.keys(controls)) {\n if (controls[i] instanceof UntypedFormControl) {\n controls[i].markAsTouched();\n } else if (controls[i] instanceof UntypedFormGroup) {\n this.setAsTouched(controls[i] as UntypedFormGroup);\n }\n controls[i].updateValueAndValidity();\n }\n form.updateValueAndValidity();\n }\n\n /**\n * returns whether or not the sales order form is valid and touches all controls\n */\n public validateForm(): boolean {\n const valid = this.parentForm.valid;\n if (!valid) {\n this.setAsTouched(this.parentForm);\n }\n return valid;\n }\n\n public openDropdown(): void {\n this.orderFormComponent.openDropdown();\n }\n\n updateAnswers(): Observable {\n if (this.orderStatus !== Status.DRAFTED && !this.validateForm()) {\n this.alertService.openErrorSnack('FRONTEND.SALES_UI.ERRORS.ERROR_FORM_NOT_VALID');\n return of(false);\n }\n\n this.submitting = true;\n const customFormData = this.getCustomFormData();\n const extraFieldsData = this.getExtraFieldsData();\n const commonFormData = this.getCommonFormData();\n\n const result$$ = new Subject();\n this.salesOrderFormService\n .updateAnswers(commonFormData, customFormData, extraFieldsData)\n .pipe(take(1))\n .subscribe(\n () => {\n result$$.next(true);\n this.parentForm.markAsPristine();\n this.submitting = false;\n this.cdr.detectChanges();\n this.alertService.openSuccessSnack('FRONTEND.SALES_UI.ORDER_FORM_ANSWERS_UPDATED');\n },\n () => {\n result$$.next(false);\n this.submitting = false;\n this.cdr.detectChanges();\n this.alertService.openErrorSnack('FRONTEND.SALES_UI.ERRORS.ERROR_UPDATING_ORDER_FORM');\n },\n );\n return result$$.asObservable();\n }\n\n refresh(o: Order): void {\n this.salesOrderFormService.refresh(o);\n this.cdr.detectChanges();\n }\n}\n", "
\n \n \n \n \n {{ 'COMMON.ORDERS.ORDER_FORM_LABEL' | translate }}\n \n \n
\n
\n
\n \n \n
\n
\n \n {{ 'COMMON.ORDERS.UPDATE_ORDER_FORM_ANSWERS' | translate }}\n \n
\n \n
\n
\n
\n
\n
\n\n\n\n
\n
\n", "import { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';\nimport { UntypedFormControl } from '@angular/forms';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { SalesOrdersService } from '@vendasta/sales-orders';\nimport { Observable, Subscription } from 'rxjs';\nimport { debounceTime, distinctUntilChanged } from 'rxjs/operators';\n\ntype NoteType = 'customer' | 'administrator';\n\ntype EditingType = 'toggleable' | 'editingonly' | 'disabled';\n\n@Component({\n selector: 'orders-notes',\n templateUrl: 'notes.component.html',\n styleUrls: ['notes.component.scss'],\n})\nexport class NotesComponent implements OnInit, OnDestroy {\n @Input() noteType: NoteType;\n @Input() existingNotes: string;\n @Input() orderId: string;\n @Input() businessId: string;\n @Input() editingType: EditingType = 'toggleable';\n @Input() createMode = false;\n\n @ViewChild('textFocus') textFocus: ElementRef;\n\n editMode = false;\n formControl: UntypedFormControl;\n notes: string;\n updatingNotes = false;\n\n subscriptions: Subscription[] = [];\n\n constructor(\n private readonly snack: SnackbarService,\n private readonly salesOrdersService: SalesOrdersService,\n private cdr: ChangeDetectorRef,\n ) {}\n\n ngOnInit(): void {\n this.notes = this.existingNotes;\n this.formControl = new UntypedFormControl(this.existingNotes, []);\n this.subscriptions.push(\n this.formControl.valueChanges.pipe(distinctUntilChanged(), debounceTime(500)).subscribe((notes) => {\n this.notes = notes;\n this.saveNotes();\n }),\n );\n this.editMode = this.editingType === 'editingonly';\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((sub) => sub.unsubscribe());\n }\n\n saveNotes(): void {\n if (this.existingNotes === this.notes || this.createMode) {\n // Skip if there were no changes to the notes\n return;\n }\n this.updatingNotes = true;\n this.updateNotes(this.notes).subscribe({\n next: (successful) => {\n if (successful) {\n this.updatingNotes = false;\n this.existingNotes = this.notes;\n }\n },\n error: () => {\n this.snack.openErrorSnack('COMMON.ORDER_DETAILS.NOTES_UPDATE_ERROR');\n this.updatingNotes = false;\n },\n });\n }\n\n onBlur(): void {\n this.editMode = this.editingType === 'editingonly';\n }\n\n onClickEdit(): void {\n this.editMode = true;\n this.cdr.detectChanges();\n this.textFocus.nativeElement.focus();\n }\n\n updateNotes(existingNotes: string): Observable {\n if (this.noteType === 'customer') {\n return this.salesOrdersService.updateCustomerNotes(this.orderId, this.businessId, existingNotes);\n } else {\n return this.salesOrdersService.updateNotes(this.orderId, this.businessId, existingNotes);\n }\n }\n\n // getNotes can be called from a ViewChild reference to get the current notes value\n getNotes(): string {\n return this.notes;\n }\n}\n", "\n \n \n
\n {{ 'COMMON.ORDER_DETAILS.CUSTOMER_NOTES_TITLE' | translate }}\n
\n \n {{ 'COMMON.ORDER_DETAILS.ADMINISTRATIVE_NOTES_TITLE' | translate }}\n \n
\n \n
\n
\n \n
\n
\n
\n\n \n \n \n @switch (noteType) {\n @case ('customer') {\n \n {{ 'COMMON.ORDER_DETAILS.CUSTOMER_NOTES_INFO' | translate }}\n \n }\n @case ('administrator') {\n \n {{ 'COMMON.ORDER_DETAILS.ADMINISTRATIVE_NOTES_INFO' | translate }}\n \n }\n }\n \n\n
\n
\n {{ existingNotes }}\n
\n \n
\n {{ 'COMMON.ORDER_DETAILS.EMPTY_NOTES' | translate }}\n
\n
\n
\n
\n
\n", "import { COMMA, ENTER } from '@angular/cdk/keycodes';\nimport { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';\nimport { UntypedFormControl } from '@angular/forms';\nimport { MatAutocomplete } from '@angular/material/autocomplete';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { Order, SalesOrdersService } from '@vendasta/sales-orders';\nimport { BehaviorSubject, Observable, ReplaySubject, Subscription, of } from 'rxjs';\nimport { debounceTime, map, switchMap } from 'rxjs/operators';\nimport { NewOrderDetails } from './tags.interface';\n\nexport type EditingType = 'toggleable' | 'editingonly' | 'disabled';\n\n@Component({\n selector: 'orders-tags',\n templateUrl: 'tags.component.html',\n styleUrls: ['tags.component.scss'],\n})\nexport class TagsComponent implements OnInit, OnDestroy {\n // Exactly one of the following inputs is required:\n // newOrderDetails is to be used when there isn't an existing order to pull info from (eg. partner id and market id)\n @Input() newOrderDetails: NewOrderDetails;\n // otherwise the existingOrder can be passed in which has all the info this component needs\n @Input() existingOrder: Order;\n @Input() editingType: EditingType = 'toggleable';\n @Input() createMode = false;\n\n @ViewChild('tagsInput') tagsInput: ElementRef;\n @ViewChild('auto') matAutocomplete: MatAutocomplete;\n\n partnerId: string;\n marketId: string;\n\n editMode = false;\n editingTags: string[] = [];\n tagOptions$: Observable;\n updatingTags = false;\n\n separatorKeysCodes: number[] = [ENTER, COMMA];\n tagsCtrl = new UntypedFormControl();\n loadTags$$ = new BehaviorSubject(true);\n newTag$$ = new ReplaySubject(1);\n newTag$ = this.newTag$$.asObservable();\n filteredTags$: Observable;\n filteredTags: string[];\n tags: string[] = [];\n availableTags: string[] = [];\n subscriptions: Subscription[] = [];\n\n constructor(\n private snack: SnackbarService,\n private salesOrderService: SalesOrdersService,\n private cdr: ChangeDetectorRef,\n ) {}\n\n ngOnInit(): void {\n this.editMode = this.editingType === 'editingonly';\n if (this.newOrderDetails) {\n this.initNewOrderTags();\n } else {\n this.initExistingOrderTags();\n }\n this.tagOptions$ = this.loadTags$$.pipe(\n switchMap(() => {\n return this.salesOrderService.listTags(this.partnerId, { marketIds: [this.marketId] });\n }),\n map((resp) => {\n return resp.tags || [];\n }),\n );\n\n this.subscriptions.push(\n // init all tags dropdown\n this.tagOptions$.subscribe((tags) => {\n // if we have pre-selected tags\n this.availableTags = tags;\n }),\n );\n\n this.filteredTags$ = this.tagsCtrl.valueChanges.pipe(\n debounceTime(300),\n map((tag: string | null) => {\n if (!tag) {\n tag = '';\n }\n const filteredTags = this._filter(tag);\n // if no existing tags found, display the new entry as an option:\n if (filteredTags.length === 0) {\n this.newTag$$.next(tag);\n return [];\n }\n this.newTag$$.next('');\n return tag ? filteredTags : this.availableTags.slice();\n }),\n );\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((sub) => sub.unsubscribe());\n }\n\n onClickEdit(): void {\n this.editMode = true;\n this.cdr.detectChanges();\n this.tagsInput.nativeElement.focus();\n }\n\n // initializes class variables with information provided via an existing order\n private initExistingOrderTags(): void {\n this.editingTags = this.existingOrder.tags;\n if (this.editingTags) {\n this.tags = this.editingTags;\n }\n this.partnerId = this.existingOrder.partnerId;\n this.marketId = this.existingOrder.marketId;\n }\n\n // initializes class variables with info provided via the details input\n private initNewOrderTags(): void {\n this.partnerId = this.newOrderDetails.partnerId;\n this.marketId = this.newOrderDetails.marketId;\n }\n\n getTags(): string[] {\n return this.tags || []; // the tag input may not have been toggled\n }\n\n remove(tag: string): void {\n const index = this.tags.findIndex((tagOption) => tagOption === tag);\n if (index >= 0) {\n this.tags.splice(index, 1);\n this.updateTags();\n }\n }\n\n onAddTag(tag: string): void {\n const index = this.tags.findIndex((tagOption) => tagOption === tag);\n if (index === -1) {\n this.tags.push(tag);\n this.updateTags();\n }\n this.tagsInput.nativeElement.value = '';\n this.tagsCtrl.setValue(null);\n }\n\n updateTags(): void {\n if (this.createMode) {\n // Skip if component is used in create order.\n return;\n }\n this.updatingTags = true;\n this.subscriptions.push(\n this.salesOrderService\n .updateTags(this.existingOrder.orderId, this.existingOrder.businessId, this.tags)\n .pipe(\n switchMap((order) => {\n return of({\n tags: order.tags || [],\n });\n }),\n )\n .subscribe({\n next: () => {\n this.updatingTags = false;\n this.loadTags$$.next(true);\n },\n error: () => {\n this.snack.openErrorSnack('COMMON.ORDER_DETAILS.TAGS_UPDATE_ERROR');\n this.updatingTags = false;\n },\n }),\n );\n }\n\n private _filter(option: string): string[] {\n const filterValue = option.toLowerCase();\n return this.availableTags.filter((tag) => tag.toLowerCase().indexOf(filterValue) === 0);\n }\n}\n", "\n \n \n
\n {{ 'COMMON.ORDER_DETAILS.TAGS' | translate }}\n
\n
\n \n
\n
\n \n
\n
\n
\n\n \n \n \n \n \n \n \n {{ tag }}\n cancel\n \n \n \n \n \n {{ tag }}\n \n \n add\n \n {{ 'COMMON.ORDER_DETAILS.NEW_TAG' | translate: { newTag: '`' + newTag + '`' } }}\n \n \n \n \n \n \n\n \n
\n
0; else noTags\" class=\"tags\">\n
\n \n {{ tag }}\n \n
\n
\n \n
\n {{ 'COMMON.ORDER_DETAILS.EMPTY_TAGS' | translate }}\n
\n
\n
\n
\n
\n", "import { LineItem } from '@vendasta/sales-orders';\nimport { UILineItem } from '@vendasta/store';\nimport { combineLatest, Observable, of } from 'rxjs';\nimport { map, publishReplay, refCount, shareReplay, switchMap } from 'rxjs/operators';\nimport { Package, PackageService } from '@vendasta/marketplace-packages';\nimport { App, AppKey, AppPartnerService } from '@galaxy/marketplace-apps';\nimport { INVALID_ITEM } from '../../shared/constants';\nimport { convertSelectedAppToLineItem, packageToUILineItem } from '../../shared/utils';\nimport { Inject, Injectable } from '@angular/core';\nimport { AppSdkService, PackageApiService } from './line-items.interface';\nimport { MARKET_ID_TOKEN, PARTNER_ID_TOKEN } from '@vendasta/sales-ui';\n\n@Injectable()\nexport class LineItemsAdapterService {\n constructor(\n @Inject(PARTNER_ID_TOKEN) private readonly partnerId$: Observable,\n @Inject(MARKET_ID_TOKEN) private readonly marketId$: Observable,\n @Inject(PackageService) private readonly packageSdk: PackageApiService,\n @Inject(AppPartnerService) private readonly appSdk: AppSdkService,\n ) {}\n\n lineItemsToUILineItems(input: {\n lineItems$: Observable;\n partnerId?: string;\n marketId?: string;\n defaultCurrency?: string;\n }): Observable {\n const partnerId$ = input.partnerId ? of(input.partnerId) : this.partnerId$;\n const marketId$ = input.marketId ? of(input.marketId) : this.marketId$;\n\n // call out for package information\n const packages$ = input.lineItems$.pipe(\n switchMap((lineItems) => {\n const packageLineItems = lineItems.filter((li) => !!li.packageId);\n if (packageLineItems.length > 0) {\n return this.packageSdk.getMulti(packageLineItems.map((li) => li.packageId));\n }\n return of([] as Package[]);\n }),\n publishReplay(1),\n refCount(),\n );\n\n const lineItemsWithPartnerIdAndMarketId$ = combineLatest([partnerId$, marketId$, input.lineItems$]).pipe(\n map(([partnerId, marketId, lineItems]) => ({ partnerId, marketId, lineItems })),\n );\n\n // call out for app information\n const appsAndAddons$ = lineItemsWithPartnerIdAndMarketId$.pipe(\n switchMap(({ lineItems, partnerId, marketId }) => {\n const appsAndAddonsLineItems = lineItems.filter((li) => !!li.appKey);\n const appsAndAddonsKeys = appsAndAddonsLineItems.map((app) => new AppKey(app.appKey));\n if (appsAndAddonsKeys.length > 0) {\n return this.appSdk.getMulti(appsAndAddonsKeys, partnerId, marketId, {}, true);\n }\n return of([] as App[]);\n }),\n map((apps) => apps.filter((a) => !!a?.key?.appId)),\n publishReplay(1),\n refCount(),\n );\n\n // resolve the package/app information into the line item\n return combineLatest([input.lineItems$, packages$, appsAndAddons$]).pipe(\n map(([lineItems, packages, appsAndAddons]) => {\n return lineItems.map((li) => {\n let uiLineItem = INVALID_ITEM;\n if (li.packageId) {\n const pack = packages.find((p) => p.packageId === li.packageId);\n if (pack) {\n uiLineItem = packageToUILineItem(pack, input.defaultCurrency);\n }\n } else {\n const app = appsAndAddons.find(\n (a) => a.key.appId === li.appKey.appId && (a.key.editionId || '') === (li.appKey.editionId || ''),\n );\n if (app) {\n uiLineItem = convertSelectedAppToLineItem(app, input.defaultCurrency);\n }\n }\n uiLineItem.lineItem = li;\n return uiLineItem;\n });\n }),\n shareReplay(1),\n );\n }\n}\n", "import { Inject, Injectable } from '@angular/core';\nimport { BehaviorSubject, Observable, of } from 'rxjs';\nimport { map, mergeMap } from 'rxjs/operators';\nimport { App } from '@galaxy/marketplace-apps';\nimport { LineItem } from '@vendasta/sales-orders';\nimport { SalesOpportunitiesService } from '@vendasta/sales-opportunities';\nimport { Package } from '@vendasta/marketplace-packages';\nimport { LineItemsSelected, SalesOpportunity } from './line-items.interface';\nimport { convertSelectedAppToLineItem, packageToUILineItem } from '../../shared/utils';\nimport { UILineItemWithEditionPricing } from '../../shared/interface';\nimport { AccountGroup } from '@galaxy/account-group';\nimport { UILineItem } from '@vendasta/store';\nimport { MerchantService } from '@galaxy/billing';\nimport { LineItemsAdapterService } from './line-items-adapter.service';\n\n@Injectable()\nexport class LineItemsService {\n partnerId: string;\n marketId: string;\n businessId: string;\n\n specifiedLineItems$$: BehaviorSubject = new BehaviorSubject([]);\n specifiedLineItems$: Observable = this.specifiedLineItems$$.asObservable();\n uiLineItems$: Observable;\n\n loading$$: BehaviorSubject = new BehaviorSubject(true);\n loading$: Observable = this.loading$$.asObservable();\n\n // Find packages and apps that have been deselected and remove them from the service instance\n public static removeUnselectedLineItems(\n preselectedPackages: Package[],\n preselectedApps: App[],\n newlySelectedLineItems: LineItemsSelected,\n lineItemsToUpdate: UILineItemWithEditionPricing[],\n ): UILineItemWithEditionPricing[] {\n // Find packages that have been deselected in the selector\n const deselectedPackageIds = [];\n preselectedPackages.map((pkg) => {\n const stillSelected = newlySelectedLineItems.packages.some((selected) => selected.packageId === pkg.packageId);\n if (!stillSelected) {\n deselectedPackageIds.push(pkg.packageId);\n }\n });\n\n // Find apps that have been deselected in the selector\n const deselectedAppIds = [];\n preselectedApps.map((app) => {\n const stillSelected = newlySelectedLineItems.apps.some((selected) => selected.key.appId === app.key.appId);\n if (!stillSelected) {\n deselectedAppIds.push(app.key.appId);\n }\n });\n\n // Remove packages and apps that have been deselected in the selector\n return lineItemsToUpdate.filter(\n (item) =>\n (!!item.lineItem.packageId && !deselectedPackageIds.includes(item.lineItem.packageId)) ||\n (!!item.lineItem.appKey?.appId && !deselectedAppIds.includes(item.lineItem.appKey.appId)),\n );\n }\n\n constructor(\n @Inject(SalesOpportunitiesService) private readonly salesOpportunityService: SalesOpportunity,\n private readonly merchantService: MerchantService,\n private readonly lineItemsAdapterService: LineItemsAdapterService,\n ) {}\n\n // initializes service class variables\n initialize(business: Pick, lineItems: LineItem[]): void {\n this.marketId = business.externalIdentifiers.marketId;\n this.partnerId = business.externalIdentifiers.partnerId;\n this.specifiedLineItems$$.next(lineItems || []);\n this.buildLineItemsFromMarketplaceInfo();\n }\n\n // populates line item branding info from marketplace package and/or app APIs\n private buildLineItemsFromMarketplaceInfo(): void {\n this.uiLineItems$ = this.lineItemsAdapterService.lineItemsToUILineItems({\n lineItems$: this.specifiedLineItems$,\n partnerId: this.partnerId,\n marketId: this.marketId,\n });\n }\n\n addSpecifiedLineItems(lineItems: LineItem[]): void {\n this.specifiedLineItems$$.next(this.specifiedLineItems$$.getValue().concat(lineItems));\n }\n\n private getDefaultCurrency(): Observable {\n if (this.marketId && this.partnerId) {\n return this.merchantService.getMultiRetailConfigurations(this.partnerId, [this.marketId]).pipe(\n map((retailConfigs) => {\n return retailConfigs.get(this.marketId).currencyCode;\n }),\n );\n }\n return of('');\n }\n\n // resolves the latest line items according to what was selected\n public resolveLineItemSelectorInteractions(\n preselectedPackages: Package[],\n preselectedApps: App[],\n newlySelectedLineItems: LineItemsSelected,\n lineItemsToUpdate: UILineItemWithEditionPricing[],\n ): Observable {\n lineItemsToUpdate = LineItemsService.removeUnselectedLineItems(\n preselectedPackages,\n preselectedApps,\n newlySelectedLineItems,\n lineItemsToUpdate,\n );\n const addNewlySelectedPackages$ = this.addNewlySelectedPackages(newlySelectedLineItems, lineItemsToUpdate);\n return addNewlySelectedPackages$.pipe(\n mergeMap((lineItems) => {\n return this.addNewlySelectedApps(newlySelectedLineItems, lineItems);\n }),\n );\n }\n\n // Add only newly selected packages so that previously selected packages do not have quantity and revenue overwritten\n public addNewlySelectedPackages(\n newlySelectedLineItems: LineItemsSelected,\n lineItemsToUpdate: UILineItemWithEditionPricing[],\n ): Observable {\n return this.getDefaultCurrency().pipe(\n map((currency) => {\n const addedPackages = newlySelectedLineItems.packages.reduce((added, pkg) => {\n if (!lineItemsToUpdate.some((item) => item.lineItem.packageId === pkg.packageId)) {\n added.push(pkg);\n }\n return added;\n }, []);\n return lineItemsToUpdate.concat(addedPackages.map((pkg) => packageToUILineItem(pkg, currency)));\n }),\n );\n }\n\n // Add only newly added apps so that preselected apps that have not be deselected do not have quantity and revenue overwritten\n public addNewlySelectedApps(\n newlySelectedLineItems: LineItemsSelected,\n lineItemsToUpdate: UILineItemWithEditionPricing[],\n ): Observable {\n return this.getDefaultCurrency().pipe(\n map((currency) => {\n const addedApps = newlySelectedLineItems.apps.reduce((added, app) => {\n if (!lineItemsToUpdate.some((item) => item.lineItem.appKey?.appId === app.key.appId)) {\n added.push(app);\n } else {\n const mustUpdateIndex = lineItemsToUpdate.findIndex((u) => u.lineItem.appKey?.appId === app.key.appId);\n if (\n mustUpdateIndex > -1 &&\n lineItemsToUpdate[mustUpdateIndex].lineItem.appKey?.editionId !== app.key.editionId\n ) {\n lineItemsToUpdate[mustUpdateIndex] = convertSelectedAppToLineItem(app, currency);\n }\n }\n return added;\n }, []);\n return lineItemsToUpdate.concat(addedApps.map((app) => convertSelectedAppToLineItem(app, currency)));\n }),\n );\n }\n}\n", "import { InventoryItemWithPricing } from '@galaxy/inventory-ui';\nimport { AppType } from '@vendasta/marketplace-apps';\nimport { LineItem, LineItemAppKey } from '@vendasta/sales-orders';\nimport { UILineItemWithEditionPricing } from '../../shared/interface';\n\nexport function inventoryItemsToLineItems(items: InventoryItemWithPricing[]): UILineItemWithEditionPricing[] {\n return items.map((item) => {\n const lineItem = new LineItem(item.pricing);\n let editionName: string = undefined;\n let appType: AppType = undefined;\n if (item.type === 'app') {\n editionName = item.edition?.name;\n }\n if (item.type !== 'package') {\n if (item.type === 'app') {\n lineItem.appKey = new LineItemAppKey({\n appId: item.itemId,\n editionId: item.edition?.editionId,\n });\n appType = AppType.APP_TYPE_APP;\n } else {\n lineItem.appKey = new LineItemAppKey({\n appId: item.itemId,\n });\n appType = AppType.APP_TYPE_ADDON;\n }\n } else {\n lineItem.packageId = item.itemId;\n }\n return {\n lineItem: lineItem,\n name: item.name,\n editionName: editionName,\n iconUrl: item.iconUrl,\n appType: appType,\n };\n });\n}\n", "import { getCurrencySymbol } from '@angular/common';\nimport { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output, signal } from '@angular/core';\nimport {\n UntypedFormArray,\n UntypedFormBuilder,\n UntypedFormControl,\n UntypedFormGroup,\n ValidationErrors,\n Validators,\n} from '@angular/forms';\nimport { MatDialog } from '@angular/material/dialog';\nimport { AccountGroup } from '@galaxy/account-group';\nimport {\n InventoryItemWithPricing,\n ItemPricingTableConfig,\n ItemPricingTableSelectorConfig,\n ItemRowChangeEvent,\n ItemsChangedEvent,\n ItemSelectorConfig,\n ItemSelectorFilterType,\n UpdateLineItemFromItemRow,\n} from '@galaxy/inventory-ui';\nimport { App } from '@galaxy/marketplace-apps';\nimport { FeatureFlagService, FeatureFlagStatusInterface } from '@galaxy/partner';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { Package } from '@vendasta/marketplace-packages';\nimport { LineItem, Revenue, RevenueComponent, RevenuePeriod, SalesOrdersService } from '@vendasta/sales-orders';\nimport { SalesOrderLineItemSelectorDialogComponent } from '@vendasta/sales-ui';\nimport { OrderFormOptionsInterface } from '@vendasta/store';\nimport { BehaviorSubject, Observable, Subscription } from 'rxjs';\nimport { debounceTime, filter, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';\nimport { UILineItemWithEditionPricing } from '../../shared/interface';\nimport { PeriodOptions } from './line-items.interface';\nimport { LineItemsService } from './line-items.service';\nimport { inventoryItemsToLineItems } from './utils';\n\nconst ITEM_PRICING_TABLE_ORDERS = 'item_pricing_table_orders';\n\n@Component({\n selector: 'orders-line-items',\n templateUrl: './line-items.component.html',\n styleUrls: ['./line-items.component.scss'],\n})\nexport class LineItemsComponent implements OnInit, OnDestroy {\n @Input() business: Pick;\n\n // we specify line items to populate the component when an opportunity is\n // selected, or when we are editing an existing order\n @Input() specifiedLineItems: LineItem[];\n\n // configurations\n hideSellingStandaloneProducts = false;\n // allows multiple quantities to be set for products that are not multi-activatable\n @Input() bypassQuantityRestrictions = false;\n @Input() allowNegativeValues = false;\n @Input() editable = false;\n @Input() startEditing = false;\n @Input() currencyOverride: string = null;\n @Input() allowRemovingLastLineItem: boolean;\n\n @Output() lineItemsUpdated: EventEmitter = new EventEmitter<\n UILineItemWithEditionPricing[]\n >();\n @Output() lineItemsInitialized: EventEmitter = new EventEmitter<\n UILineItemWithEditionPricing[]\n >();\n\n editing = false;\n loading$: Observable;\n formArrayLineItems: UntypedFormArray;\n currentLineItems$$: BehaviorSubject = new BehaviorSubject<\n UILineItemWithEditionPricing[]\n >([]);\n currentLineItems: UILineItemWithEditionPricing[];\n uiLineItems$: Observable;\n orderFormOptions$: Observable;\n featureFlags$: Observable;\n\n useInventoryPricingTableSelector = signal(false);\n useInventoryItemPricingTableSelector$: Observable;\n itemPricingTableSelectorConfig$: Observable;\n\n // pricing related\n periodOptions: PeriodOptions[] = [\n { label: 'FRONTEND.STORE.PERIODS.ONE_TIME', value: RevenuePeriod.ONETIME },\n { label: 'FRONTEND.STORE.PERIODS.MONTHLY', value: RevenuePeriod.MONTHLY },\n { label: 'FRONTEND.STORE.PERIODS.YEARLY', value: RevenuePeriod.YEARLY },\n ];\n\n oneTimeOnlyOptions: PeriodOptions[] = [{ label: 'FRONTEND.STORE.PERIODS.ONE_TIME', value: RevenuePeriod.ONETIME }];\n\n private subscriptions: Subscription[] = [];\n\n constructor(\n private formbuilder: UntypedFormBuilder,\n private cdk: ChangeDetectorRef,\n private dialog: MatDialog,\n private alertService: SnackbarService,\n private readonly salesOrdersService: SalesOrdersService,\n private readonly lineItemsService: LineItemsService,\n private readonly featureFlagService: FeatureFlagService,\n ) {}\n\n ngOnInit(): void {\n this.editing = !!this.editable && !!this.startEditing;\n this.lineItemsService.initialize(this.business, this.specifiedLineItems);\n this.uiLineItems$ = this.lineItemsService.uiLineItems$.pipe(shareReplay(1));\n this.loading$ = this.lineItemsService.loading$;\n const orderConfig$ = this.salesOrdersService\n .getConfig(this.business.externalIdentifiers.partnerId, this.business.externalIdentifiers.marketId)\n .pipe(shareReplay(1));\n const hideSellingStandaloneProducts$ = orderConfig$.pipe(\n map((config) => config.salespersonOptions?.disableSellingStandaloneProducts || false),\n );\n this.subscriptions.push(\n hideSellingStandaloneProducts$.subscribe((hideSellingStandaloneProducts) => {\n this.hideSellingStandaloneProducts = hideSellingStandaloneProducts;\n }),\n );\n this.formArrayLineItems = this.formbuilder.array([]);\n this.subscriptions.push(\n this.uiLineItems$\n .pipe(\n map((uiLineItems) => {\n this.setCurrentLineItems(uiLineItems);\n this.lineItemsInitialized.emit(uiLineItems);\n this.formArrayLineItems = this.buildFormArrayFromLineItems(uiLineItems);\n this.cdk.detectChanges();\n }),\n tap(() => this.lineItemsService.loading$$.next(false)),\n )\n .subscribe(),\n );\n this.orderFormOptions$ = orderConfig$.pipe(\n map((config): OrderFormOptionsInterface => {\n return {\n readOnly: false,\n showOfficeUseQuestions: true,\n bypassRequiredQuestions: !config.salespersonOptions.validateRequiredFields,\n };\n }),\n );\n\n this.featureFlags$ = this.featureFlagService\n .batchGetStatus(this.business.externalIdentifiers.partnerId, this.business.externalIdentifiers.marketId, [\n ITEM_PRICING_TABLE_ORDERS,\n ])\n .pipe(shareReplay({ bufferSize: 1, refCount: true }));\n this.useInventoryItemPricingTableSelector$ = this.featureFlags$.pipe(\n map((flags) => flags[ITEM_PRICING_TABLE_ORDERS]),\n tap((useInventoryPricingTable) => this.useInventoryPricingTableSelector.set(useInventoryPricingTable)),\n );\n\n this.itemPricingTableSelectorConfig$ = hideSellingStandaloneProducts$.pipe(\n map((hideSellingStandaloneProducts) => {\n return {\n itemPricingTableConfig: this.itemPricingTableConfig(),\n itemSelectorConfig: this.itemSelectorConfig(hideSellingStandaloneProducts),\n items: [], // TODO - provide the items for creating orders from opportunities and editing orders.\n };\n }),\n );\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((subscription) => subscription.unsubscribe());\n }\n\n deleteLineItem(index: number): void {\n if (!this.allowRemovingLastLineItem && this.currentLineItems.length === 1) {\n this.alertService.openErrorSnack('COMMON.ORDERS.ERROR_AT_LEAST_ONE_LINE_ITEM');\n return;\n }\n this.currentLineItems.splice(index, 1);\n this.setCurrentLineItems(this.currentLineItems);\n this.formArrayLineItems.removeAt(index);\n this.lineItemsUpdated.emit(this.currentLineItems);\n }\n\n duplicateLineItem(index: number): void {\n const duplicateLineItem = structuredClone(this.currentLineItems[index]);\n this.currentLineItems.splice(index + 1, 0, duplicateLineItem);\n this.setCurrentLineItems(this.currentLineItems);\n // update line items in the UI and form\n this.formArrayLineItems = this.buildFormArrayFromLineItems(this.currentLineItems);\n this.cdk.detectChanges();\n this.lineItemsUpdated.emit(this.currentLineItems);\n }\n\n addLineItems(): void {\n const preselectedPackages = this.currentLineItems\n .filter((item) => !!item.lineItem?.packageId)\n .map((item) => new Package({ packageId: item.lineItem.packageId }));\n const preselectedApps = this.currentLineItems\n .filter((item) => !!item.lineItem?.appKey?.appId)\n .map(\n (item) =>\n new App({\n key: {\n appId: item.lineItem.appKey.appId,\n editionId: item.lineItem.appKey.editionId,\n },\n parentRequirements: {\n parentDetails: {\n key: {\n appId: item.parentAppId,\n },\n name: item.parentName,\n },\n },\n appType: item.appType,\n }),\n );\n\n const dialogRef = this.dialog.open(SalesOrderLineItemSelectorDialogComponent, {\n data: {\n marketId: this.business.externalIdentifiers.marketId,\n preselectedApps: preselectedApps,\n preselectedPackages: preselectedPackages,\n hideSellingStandaloneProducts: this.hideSellingStandaloneProducts,\n hideOwnerName: true,\n itemSelectorIncludesPackages: true, // TODO - remove this when the item selector defaults to showing packages\n },\n width: '1000px',\n maxWidth: '95vw',\n height: '900px',\n minHeight: '420px',\n maxHeight: '85vh',\n });\n dialogRef\n .afterClosed()\n .pipe(\n take(1),\n filter((newlySelectedLineItems) => !!newlySelectedLineItems),\n tap(() => this.updateFormValuesWithEditingItems()),\n switchMap((newlySelectedLineItems) => {\n return this.lineItemsService.resolveLineItemSelectorInteractions(\n preselectedPackages,\n preselectedApps,\n newlySelectedLineItems,\n this.currentLineItems,\n );\n }),\n switchMap((newlySelectedLineItems) => {\n this.setCurrentLineItems(newlySelectedLineItems);\n // update line items in the UI and form\n this.formArrayLineItems = this.buildFormArrayFromLineItems(newlySelectedLineItems);\n this.subscriptions.push(\n this.formArrayLineItems.valueChanges.pipe(debounceTime(1000)).subscribe(() => {\n this.updateFormValuesWithEditingItems();\n }),\n );\n return newlySelectedLineItems;\n }),\n )\n .subscribe(() => {\n // scroll view to the top of the line item editor as updated list of items increases the vertical height of the view\n document.getElementById('top').scrollIntoView({ behavior: 'smooth' });\n this.cdk.detectChanges();\n this.lineItemsUpdated.emit(this.currentLineItems);\n });\n }\n\n updateFormValuesWithEditingItems(): void {\n // bypass forms when using inventory pricing table\n if (this.useInventoryPricingTableSelector()) return;\n\n this.currentLineItems.map((uiLineItem, index) => {\n const lineItemFormGroup = this.formArrayLineItems.controls[index] as UntypedFormGroup;\n uiLineItem.lineItem.quantity = lineItemFormGroup.controls.quantity.value;\n const revenueComponentsFormArray = lineItemFormGroup.controls?.revenueComponents as UntypedFormArray;\n const revenueComponents = revenueComponentsFormArray.controls.map((fa) => {\n const fg = fa as UntypedFormGroup;\n return new RevenueComponent({\n value: Math.round(fg.controls.revenue.value * 100),\n period: fg.controls.period.value,\n isStartingRevenue: fg.controls.startingAt.value,\n });\n });\n uiLineItem.lineItem.currentRevenue = new Revenue({ revenueComponents: revenueComponents });\n });\n }\n\n // builds the form group out of the selected items\n buildFormArrayFromLineItems(lineItems: UILineItemWithEditionPricing[]): UntypedFormArray {\n const newForm = this.formbuilder.array(\n lineItems.map((li) => {\n const revenueControls = this.revenueControlFromLineItemRevenue(li.lineItem?.currentRevenue);\n const quantityEnabled = this.bypassQuantityRestrictions || (li.isMultiActivatable && !li.usesCustomPricing);\n return this.formbuilder.group({\n quantity: [\n {\n value: quantityEnabled ? li.lineItem.quantity : 1,\n disabled: !quantityEnabled,\n },\n [Validators.min(1)],\n ],\n revenueComponents: this.formbuilder.array(revenueControls),\n });\n }),\n );\n\n // TODO: This is a workaround for the form array being rebuilt when adding and removing items. We will probably want\n // to take the time in the future refactoring this component so it doesn't leak subscriptions\n this.subscriptions.push(\n newForm.valueChanges.pipe(debounceTime(500)).subscribe(() => this.lineItemsUpdated.emit(this.currentLineItems)),\n );\n\n return newForm;\n }\n\n revenueControlFromLineItemRevenue(revenue: Revenue): UntypedFormGroup[] {\n return (revenue?.revenueComponents || []).map((rc) =>\n this.formbuilder.group({\n revenue: [(rc.value || 0) / 100, [Validators.required, this.validateRevenue.bind(this)]],\n period: [rc.period === undefined ? RevenuePeriod.ONETIME : rc.period, [Validators.required]],\n startingAt: [rc.isStartingRevenue],\n }),\n );\n }\n\n validateRevenue(c: UntypedFormControl): ValidationErrors | null {\n const revenueValue = c.value;\n if (isNaN(revenueValue)) {\n return { pattern: true };\n }\n if (Number(revenueValue) < 0 && this.allowNegativeValues === false) {\n return { min: true };\n }\n return null;\n }\n\n //\n // NOTE:\n // The following methods are used from page components with a ViewChild reference to this component\n //\n\n // check that the form of the component is in a valid state\n public isFormValid(): boolean {\n const valid = this.formArrayLineItems.valid;\n if (!valid) {\n this.formArrayLineItems.markAsTouched();\n this.formArrayLineItems.updateValueAndValidity();\n }\n if (this.currentLineItems.length <= 0) {\n this.alertService.openErrorSnack('FRONTEND.SALES_UI.ERRORS.ERROR_MUST_HAVE_ITEMS');\n return false;\n }\n if (this.currentLineItems.find((item) => item.lineItem?.currentRevenue?.revenueComponents.length === 0)) {\n this.alertService.openErrorSnack('FRONTEND.SALES_UI.ERRORS.ERROR_MUST_HAVE_PRICING');\n return false;\n }\n\n return valid;\n }\n\n // retrieve the line items currently stored in the component\n public getCurrentLineItems(): LineItem[] {\n this.updateFormValuesWithEditingItems();\n return this.currentLineItems.map((li) => {\n const item = new LineItem(li.lineItem);\n if (this.currencyOverride) {\n item.currencyCode = this.currencyOverride;\n }\n return item;\n });\n }\n\n public currencySymbol(currencyCode: string): string {\n return getCurrencySymbol(currencyCode, 'wide');\n }\n\n addRevenueRow(liIndex: number): void {\n const components = this.currentLineItems[liIndex].lineItem.currentRevenue.revenueComponents;\n components.push(new RevenueComponent());\n this.formArrayLineItems = this.buildFormArrayFromLineItems(this.currentLineItems);\n this.subscriptions.push(\n this.formArrayLineItems.valueChanges.pipe(debounceTime(1000)).subscribe(() => {\n this.updateFormValuesWithEditingItems();\n }),\n );\n this.cdk.detectChanges();\n this.lineItemsUpdated.emit(this.currentLineItems);\n }\n\n deleteRevenueRow(liIndex: number, rcIndex: number): void {\n if (\n !this.allowRemovingLastLineItem &&\n this.currentLineItems[liIndex].lineItem.currentRevenue.revenueComponents.length === 1 &&\n this.currentLineItems.length === 1\n ) {\n this.alertService.openErrorSnack('COMMON.ORDERS.ERROR_AT_LEAST_ONE_LINE_ITEM');\n return;\n }\n\n this.currentLineItems[liIndex].lineItem.currentRevenue.revenueComponents.splice(rcIndex, 1);\n if (this.currentLineItems[liIndex].lineItem.currentRevenue.revenueComponents.length === 0) {\n this.currentLineItems.splice(liIndex, 1);\n }\n this.formArrayLineItems = this.buildFormArrayFromLineItems(this.currentLineItems);\n this.updateFormValuesWithEditingItems();\n this.subscriptions.push(\n this.formArrayLineItems.valueChanges.pipe(debounceTime(1000)).subscribe(() => {\n this.updateFormValuesWithEditingItems();\n }),\n );\n this.cdk.detectChanges();\n this.lineItemsUpdated.emit(this.currentLineItems);\n }\n\n private setCurrentLineItems(uiLineItems: UILineItemWithEditionPricing[]): void {\n // Sort the revenue components to ensure one time always appears last, initially\n uiLineItems?.forEach?.((uilineItem) => {\n uilineItem.lineItem?.currentRevenue?.revenueComponents?.sort((a: RevenueComponent, b: RevenueComponent) => {\n if (a.period === RevenuePeriod.ONETIME || !a.period) {\n return 1;\n }\n if (b.period === RevenuePeriod.ONETIME || !b.period) {\n return -1;\n }\n return 0;\n });\n });\n this.currentLineItems = uiLineItems;\n this.currentLineItems$$.next(uiLineItems);\n }\n\n updateLineItemFromInventoryItem(event: ItemRowChangeEvent): void {\n if (event.action === 'add') {\n this.duplicateLineItem(event.item.index);\n } else if (event.action === 'update') {\n this.currentLineItems[event.item.index].lineItem = UpdateLineItemFromItemRow(\n this.currentLineItems[event.item.index].lineItem,\n event.item,\n );\n } else if (event.action === 'delete') {\n this.deleteLineItem(event.item.index);\n return;\n }\n this.setCurrentLineItems(this.currentLineItems);\n this.lineItemsUpdated.emit(this.currentLineItems);\n }\n\n updateLineItemsFromInventoryLineItems(changedEvent: ItemsChangedEvent): void {\n let updateFn: (currentLineItems: UILineItemWithEditionPricing[]) => UILineItemWithEditionPricing[];\n switch (changedEvent.type) {\n case 'add':\n updateFn = (currentLineItems) => {\n const newItems = changedEvent.items;\n const newLineItems = inventoryItemsToLineItems(newItems);\n newItems.forEach((newItem, i) => {\n currentLineItems.splice(newItem.index, 0, newLineItems[i]);\n });\n return currentLineItems;\n };\n break;\n case 'update':\n updateFn = (currentLineItems) => {\n const changedItems = changedEvent.items;\n const changedLineItems = inventoryItemsToLineItems(changedItems);\n changedItems.forEach((changedItem, i) => {\n const lineItem = changedLineItems[i];\n currentLineItems[changedItem.index] = lineItem;\n });\n return currentLineItems;\n };\n break;\n case 'delete':\n updateFn = (currentLineItems) => {\n return currentLineItems.filter((_, index) => !changedEvent.items.some((item) => item.index === index));\n };\n break;\n }\n this.setCurrentLineItems(updateFn(this.currentLineItems));\n this.lineItemsUpdated.emit(this.currentLineItems);\n }\n\n private itemPricingTableConfig(): ItemPricingTableConfig {\n return {\n partnerId: this.business.externalIdentifiers.partnerId,\n marketId: this.business.externalIdentifiers.marketId,\n businessId: this.business.accountGroupId,\n selectionEditable: true,\n quantityEditable: true,\n billingTermsEnabled: true,\n hideColumns: ['wholesale_price', 'retail_price_total'],\n hidePriceSummary: true,\n hideBorder: true,\n };\n }\n\n private itemSelectorConfig(hideSellingStandaloneProducts: boolean): ItemSelectorConfig {\n let itemTypesAvailable: ItemSelectorFilterType[] = ['products', 'packages'];\n if (hideSellingStandaloneProducts) {\n itemTypesAvailable = itemTypesAvailable.filter((itemType) => itemType !== 'products');\n }\n return {\n partnerId: this.business.externalIdentifiers.partnerId,\n marketId: this.business.externalIdentifiers.marketId,\n businessId: this.business.accountGroupId,\n pageSize: 15,\n hideOwnerName: true,\n hideWholesale: true,\n hideRetail: false,\n hideQuantity: true,\n hideItemsWithOrderForms: false,\n itemTypesAvailable: itemTypesAvailable,\n };\n }\n}\n", "\n \n \n {{ 'COMMON.LINE_ITEMS.TITLE' | translate }}\n \n \n\n @if ((useInventoryItemPricingTableSelector$ | async) === true && !specifiedLineItems?.length) {\n \n @if ((loading$ | async) === false) {\n \n } @else {\n \n }\n \n } @else {\n \n \n
\n
\n
\n
\n \n
\n
\n
\n
\n }\n\n \n
\n
\n
\n\n \n
0; else addItemsButton\">\n \n
\n \n\n \n\n \n \n \n {{ 'COMMON.LINE_ITEMS.ITEM' | translate }}\n \n \n \n \n {{ 'COMMON.LINE_ITEMS.QUANTITY' | translate }}\n \n \n \n \n {{ 'COMMON.LINE_ITEMS.UNIT_PRICE' | translate }}\n \n \n \n \n {{ 'COMMON.LINE_ITEMS.FREQUENCY' | translate }}\n \n \n \n \n\n \n \n
\n \n \n
\n
\n \n \n
\n \n \n \n
\n {{ uiLineItem.name }}\n | {{ uiLineItem.editionName }}\n
\n
\n
\n\n \n \n \n \n \n {{ 'COMMON.LINE_ITEMS.GREATER_THAN_ZERO' | translate }}\n \n \n\n \n \n \n \n \n \n {{ 'COMMON.LINE_ITEMS.REVENUE_REQUIRED' | translate }}\n \n \n {{ 'COMMON.LINE_ITEMS.GREATER_THAN_OR_EQUAL_TO_ZERO' | translate }}\n \n \n {{ 'COMMON.LINE_ITEMS.MUST_BE_NUM' | translate }}\n \n \n \n \n \n \n \n {{ option.label | translate }}\n \n \n \n \n {{ option.label | translate }}\n \n \n \n \n \n {{ 'COMMON.LINE_ITEMS.FREQUENCY_REQUIRED' | translate }}\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 + {{ 'COMMON.LINE_ITEMS.ADD' | translate }}\n \n
\n
\n\n\n \n \n \n\n", "import { Component, Output, EventEmitter } from '@angular/core';\nimport { Currency } from '@galaxy/billing';\n\n@Component({\n selector: 'orders-currency-selector',\n templateUrl: './currency-selector.component.html',\n styleUrls: ['./currency-selector.component.scss'],\n})\nexport class CurrencySelectorComponent {\n private _selectedCurrency = '';\n\n allCurrencies: string[];\n\n get selectedCurrency(): string {\n return this._selectedCurrency;\n }\n\n set selectedCurrency(curr: string) {\n this._selectedCurrency = curr;\n this.currencySelected.emit(curr);\n }\n\n @Output() currencySelected = new EventEmitter();\n\n constructor() {\n this.allCurrencies = Object.keys(Currency).map((key) => Currency[key]);\n }\n}\n", "\n \n \n {{ 'COMMON.ORDER_CURRENCY.TITLE' | translate }}\n \n {{ 'COMMON.ORDER_CURRENCY.VMF_ONLY_BADGE' | translate }}\n \n \n \n {{ 'COMMON.ORDER_CURRENCY.DESCRIPTION' | translate }}\n \n \n\n \n \n \n \n {{ 'COMMON.ORDER_CURRENCY.DEFAULT_OPTION' | translate }}\n \n \n {{ currency }}\n \n \n \n \n\n", "import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { AppPrice } from '@vendasta/marketplace-apps';\n\nexport interface VariablePriceInputs {\n appPrice: AppPrice;\n name: string;\n iconUrl?: string;\n packageID?: string;\n packageName?: string;\n packageIconUrl?: string;\n}\n\nexport type AppVariablePriceMap = Map;\nexport type PackageVariablePriceMap = Map;\n\n@Component({\n selector: 'orders-variable-prices',\n templateUrl: './variable-prices.component.html',\n styleUrls: ['./variable-prices.component.scss'],\n})\nexport class VariablePricesComponent {\n @Input() variablePricesMap: AppVariablePriceMap;\n @Input() variablePackagePricesMap: PackageVariablePriceMap;\n @Input() partnerCurrency: string;\n @Output() updateData: EventEmitter<{ appId: string; customPrice: number; packageId?: string }> = new EventEmitter();\n\n changeSpendAmount(customPrice, appId, packageId?): void {\n this.updateData.emit({ appId: appId, customPrice: customPrice, packageId: packageId });\n }\n}\n", "\n \n \n \n \n \n {{ product.value?.name }}\n \n \n

Enter the amount you'll spend each billing period.

\n \n \n \n
\n
\n
\n
\n\n\n\n \n \n \n \n \n \n {{ product.value?.packageName }}\n {{ product.value?.name }}\n \n \n

Enter the amount you'll spend each billing period.

\n \n \n \n
\n
\n
\n
\n
\n\n", "import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';\nimport { ConfigInterface, LineItem, SalesOrdersService } from '@vendasta/sales-orders';\nimport { UntypedFormGroup } from '@angular/forms';\nimport { CreateOrderDetailsComponent } from '../../create-order-details/create-order-details.component';\nimport { AccountGroup } from '@vendasta/account-group';\nimport { FileInfo } from '@vendasta/galaxy/uploader';\nimport { SalesOrderConfirmationComponent } from '@vendasta/sales-ui';\nimport { NotesComponent } from '../../notes/notes.component';\nimport { AttachmentsComponent } from '../../attachments/attachments.component';\nimport { EnvironmentService, Environment } from '@galaxy/core';\nimport { TagsComponent } from '../../tags/tags.component';\nimport {\n AppVariablePriceMap,\n PackageVariablePriceMap,\n VariablePriceInputs,\n} from '../../variable-prices/variable-prices.component';\nimport { NewOrderDetails } from '../../tags/tags.interface';\nimport { UILineItemWithEditionPricing } from '../../../shared/interface';\nimport { App, AppKey, AppPrices, FieldMask } from '@vendasta/marketplace-apps';\nimport { catchError, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';\nimport { BehaviorSubject, Observable, of, Subject } from 'rxjs';\nimport { AppPartnerService, AppPricingService } from '@galaxy/marketplace-apps';\nimport { BillingService } from '@galaxy/billing';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Package, PackageService } from '@vendasta/marketplace-packages';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\n\n@Component({\n selector: 'orders-create-order',\n templateUrl: './create-order.component.html',\n styleUrls: ['./create-order.component.scss'],\n})\nexport class CreateOrderComponent implements OnInit, OnDestroy {\n @Input() business: AccountGroup;\n @Input() formGroup: UntypedFormGroup;\n @Input() selectedLineItems: LineItem[];\n\n @ViewChild('createOrderDetailsComponent') createOrderDetailsComponent: CreateOrderDetailsComponent;\n @ViewChild('salesOrderConfirmation') salesOrderConfirmation: SalesOrderConfirmationComponent;\n @ViewChild('adminNotes') adminNotes: NotesComponent;\n @ViewChild('customerNotes') customerNotes: NotesComponent;\n @ViewChild('attachmentsComponent') attachmentsComponent: AttachmentsComponent;\n @ViewChild('tagsComponent') tagsComponent: TagsComponent;\n\n orderDetailsFormGroup: UntypedFormGroup = new UntypedFormGroup({});\n attachmentsUploadUrl = 'https://sales-orders-prod.apigateway.co/file/upload';\n\n files: FileInfo[] = [];\n newOrderDetails: NewOrderDetails;\n currencyOverride: string = null;\n\n orderConfig$: Observable;\n variablePriceInputs$: Observable;\n partnerCurrency$: Observable;\n variablePricesMap$$ = new BehaviorSubject(new Map());\n variablePackagePricesMap$$ = new BehaviorSubject(new Map());\n private destroyed$$ = new Subject();\n\n constructor(\n private snackbarService: SnackbarService,\n private environmentService: EnvironmentService,\n private appSdk: AppPartnerService,\n private appPricingService: AppPricingService,\n private billingService: BillingService,\n private salesOrdersService: SalesOrdersService,\n private readonly packageSdk: PackageService,\n ) {}\n\n ngOnInit(): void {\n if (this.environmentService.getEnvironment() === Environment.DEMO) {\n this.attachmentsUploadUrl = 'https://sales-orders-demo.apigateway.co/file/upload';\n }\n this.newOrderDetails = {\n partnerId: this.business.accountGroupExternalIdentifiers.partnerId,\n marketId: this.business.accountGroupExternalIdentifiers.marketId,\n };\n\n this.orderConfig$ = this.salesOrdersService\n .getConfig(this.business.accountGroupExternalIdentifiers.partnerId, this.business.accountGroupId)\n .pipe(shareReplay(1));\n\n this.partnerCurrency$ = this.billingService\n .getMerchantWholesaleCurrency(this.business.accountGroupExternalIdentifiers.partnerId)\n .pipe(shareReplay(1));\n }\n\n getCreateOrderDetailsComponent(): CreateOrderDetailsComponent {\n return this.createOrderDetailsComponent;\n }\n\n getSalesOrderConfirmationComponent(): SalesOrderConfirmationComponent {\n return this.salesOrderConfirmation;\n }\n\n getAdminNotesComponent(): NotesComponent {\n return this.adminNotes;\n }\n\n getCustomerNotesComponent(): NotesComponent {\n return this.customerNotes;\n }\n\n getAttachmentsComponent(): AttachmentsComponent {\n return this.attachmentsComponent;\n }\n\n getTagsComponent(): TagsComponent {\n return this.tagsComponent;\n }\n\n appendLineItemsToCorrespondiongProductIDList(\n currentLineItems: UILineItemWithEditionPricing[],\n appIDs: string[],\n packageIDs: string[],\n ): void {\n currentLineItems.map((currentLineItem) => {\n if (currentLineItem.lineItem?.appKey?.appId) {\n appIDs.push(currentLineItem.lineItem.appKey.appId);\n } else if (currentLineItem.lineItem?.packageId) {\n packageIDs.push(currentLineItem.lineItem?.packageId);\n }\n });\n }\n\n updateAppPrices(currentLineItems: UILineItemWithEditionPricing[]): void {\n const appIDs: string[] = [];\n const packageIDs: string[] = [];\n let storedPackages: Package[] = [];\n let storedApps: App[] = [];\n\n this.appendLineItemsToCorrespondiongProductIDList(currentLineItems, appIDs, packageIDs);\n\n //Observable to get apps from packages given list of packageIDs and appIDs computed above\n const appsFromPackages$ = (\n packageIDs.length < 1 ? (of([]) as Observable) : this.packageSdk.getMulti(packageIDs)\n ).pipe(\n map((packages) => {\n storedPackages = packages;\n return this.getAppIDsFromPackage(packages);\n }),\n switchMap((appIDsFromPackages: string[]) => {\n const appKeys: AppKey[] = this.convertAppIDsIntoAppKeys(appIDsFromPackages);\n return appKeys.length < 1\n ? (of([]) as Observable)\n : this.appSdk.getMulti(appKeys, this.newOrderDetails.partnerId, this.newOrderDetails.marketId, null, true);\n }),\n );\n\n //This pipe attempts to add apps taken from packages to the appIDs list, which is then used in the GetMultiPricing call\n const pricesForAllApps$ = appsFromPackages$.pipe(\n switchMap((appsFromPackages) => {\n appsFromPackages.map((currentPackageApp) => {\n appIDs.push(currentPackageApp.key.appId);\n });\n\n storedApps = appsFromPackages;\n\n if (appIDs.length > 0) {\n return this.appPricingService.getMultiPricing(\n appIDs,\n new FieldMask({ paths: ['wholesale'] }),\n this.business.accountGroupExternalIdentifiers.partnerId,\n null,\n );\n } else {\n return of([]);\n }\n }),\n catchError((err: HttpErrorResponse) => {\n this.snackbarService.openErrorSnack('Error getting prices for order: ' + err);\n console.error(err);\n return of(null);\n }),\n );\n\n //This pipe determines whether a given app price should get external data (icon, name) from the line item, or from the app and package models\n this.variablePriceInputs$ = pricesForAllApps$.pipe(\n map((appPrices) => {\n return appPrices\n .filter((appPrice) => appPrice.pricesForContexts['wholesale'].billingConfiguration?.usesVariablePricing)\n .map((appPrice) => {\n const index = this.getIndexOfLineItem(currentLineItems, appPrice);\n if (index > -1) {\n return {\n appPrice: appPrice.pricesForContexts['wholesale'],\n name: currentLineItems[index].name,\n iconUrl: currentLineItems[index]?.iconUrl,\n } as VariablePriceInputs;\n } else {\n //App not found in line items; app must be part of a package\n const appModelData = this.findAppMatchingAppPrice(storedApps, appPrice);\n\n const packageModel = this.findPackageMatchingAppPrice(storedPackages, appPrice);\n\n return {\n appPrice: appPrice.pricesForContexts['wholesale'],\n name: appModelData.sharedMarketingInformation.name,\n iconUrl: appModelData.sharedMarketingInformation.iconUrl,\n packageID: packageModel.packageId,\n packageName: packageModel.name,\n packageIconUrl: packageModel.icon,\n } as VariablePriceInputs;\n }\n });\n }),\n shareReplay(1),\n );\n\n this.variablePriceInputs$.pipe(takeUntil(this.destroyed$$)).subscribe((variablePrices) => {\n const variablePricesMap = this.variablePricesMap$$.getValue();\n const variablePackagePricesMap = this.variablePackagePricesMap$$.getValue();\n\n // Store itemIDs of variablePriceInputs\n const variablePriceInputIds = new Set();\n\n variablePrices.forEach((variablePrice: VariablePriceInputs) => {\n if (variablePrice.packageID) {\n variablePriceInputIds.add(variablePrice.packageID);\n if (!variablePackagePricesMap.has(variablePrice.packageID)) {\n variablePackagePricesMap.set(variablePrice.packageID, new Map());\n }\n if (!variablePackagePricesMap.get(variablePrice.packageID).has(variablePrice.appPrice?.appId)) {\n variablePackagePricesMap.get(variablePrice.packageID).set(variablePrice.appPrice?.appId, variablePrice);\n }\n } else {\n variablePriceInputIds.add(variablePrice.appPrice?.appId);\n if (!variablePricesMap.has(variablePrice.appPrice?.appId)) {\n variablePricesMap.set(variablePrice.appPrice?.appId, variablePrice);\n }\n }\n });\n // Handle line items that were removed from the order by deleting keys of items\n // that are no longer in the variablePriceInputs array.\n variablePricesMap.forEach((_, key) => {\n if (!variablePriceInputIds.has(key)) {\n variablePricesMap.delete(key);\n }\n });\n variablePackagePricesMap.forEach((_, key) => {\n if (!variablePriceInputIds.has(key)) {\n variablePackagePricesMap.delete(key);\n }\n });\n\n this.variablePricesMap$$.next(new Map(variablePricesMap));\n this.variablePackagePricesMap$$.next(new Map(variablePackagePricesMap));\n });\n }\n\n updateMap(event: { appId: string; customPrice: number; packageId?: string }, partnerCurrency: string) {\n if (event.packageId) {\n const variablePricesPackageMap = this.variablePackagePricesMap$$.getValue();\n const variablePrice = variablePricesPackageMap.get(event.packageId).get(event.appId);\n variablePrice.appPrice.pricesForEditions[''].pricesForCurrencies[\n partnerCurrency\n ].pricesForFrequencies[0].pricingRules[0].price = event.customPrice;\n variablePricesPackageMap.get(event.packageId).set(event.appId, variablePrice);\n this.variablePackagePricesMap$$.next(new Map(variablePricesPackageMap));\n } else {\n const variablePricesMap = this.variablePricesMap$$.getValue();\n const variablePrice = variablePricesMap.get(event.appId);\n variablePrice.appPrice.pricesForEditions[''].pricesForCurrencies[\n partnerCurrency\n ].pricesForFrequencies[0].pricingRules[0].price = event.customPrice;\n variablePricesMap.set(event.appId, variablePrice);\n this.variablePricesMap$$.next(new Map(variablePricesMap));\n }\n }\n\n getVariableMap(): AppVariablePriceMap {\n return this.variablePricesMap$$.getValue();\n }\n\n getPackageVariableMap(): PackageVariablePriceMap {\n return this.variablePackagePricesMap$$.getValue();\n }\n\n getAppIDsFromPackage(packages: Package[]): string[] {\n const appIDsFromPackages: string[] = [];\n packages.forEach((currentPackage) => {\n currentPackage.lineItems.lineItems.forEach((currentPackageLineItem) => {\n appIDsFromPackages.push(currentPackageLineItem.id);\n });\n });\n return appIDsFromPackages;\n }\n\n convertAppIDsIntoAppKeys(appIDsFromPackages: string[]): AppKey[] {\n return appIDsFromPackages.map((currentAppID) => {\n return {\n appId: currentAppID,\n } as AppKey;\n });\n }\n\n getIndexOfLineItem(currentLineItems: UILineItemWithEditionPricing[], appPrice: AppPrices): number {\n return currentLineItems.findIndex((currentLineItem) => {\n if (currentLineItem.lineItem?.appKey?.appId) {\n return currentLineItem.lineItem.appKey.appId === appPrice.appId;\n }\n });\n }\n\n findAppMatchingAppPrice(appsFromPackages: App[], appPrice: AppPrices): App {\n return appsFromPackages.find((currentApp) => {\n return currentApp.key.appId === appPrice.appId;\n });\n }\n\n findPackageMatchingAppPrice(storedPackages: Package[], appPrice: AppPrices): Package {\n return storedPackages.find((currentPackage) => {\n return currentPackage.lineItems.lineItems.find((currentLineItem) => {\n return currentLineItem.id === appPrice.appId;\n });\n });\n }\n\n ngOnDestroy(): void {\n this.destroyed$$.next();\n this.destroyed$$.complete();\n }\n}\n", "
\n
\n
\n \n \n\n \n \n\n \n \n\n \n \n
\n\n
\n \n \n \n \n 0 || packages.size > 0\"\n #variablePricesComponent\n [variablePricesMap]=\"products\"\n [variablePackagePricesMap]=\"packages\"\n [partnerCurrency]=\"pCurrency\"\n (updateData)=\"updateMap($event, pCurrency)\"\n />\n \n \n \n\n \n \n \n\n \n \n\n \n \n \n
\n
\n
\n", "import { Injectable } from '@angular/core';\nimport { Observable, Observer } from 'rxjs';\n\nexport interface Script {\n name: string;\n src: string;\n}\n\n@Injectable()\nexport class StripeService {\n private loaded = false;\n\n public loadScript(): Observable