@finos/legend-application-marketplace
Version:
Legend Marketplace application core
368 lines • 14.3 kB
JavaScript
/**
* Copyright (c) 2025-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { makeObservable, observable, action, flow, flowResult, computed, } from 'mobx';
import { LogEvent, assertErrorThrown, ActionState, } from '@finos/legend-shared';
import { TerminalItemType, RecommendationSource, } from '@finos/legend-server-marketplace';
import { APPLICATION_EVENT } from '@finos/legend-application';
import { toastManager } from '../../components/Toast/CartToast.js';
const boolToString = (val) => val ? 'true' : 'false';
var BUSINESS_REASONS;
(function (BUSINESS_REASONS) {
BUSINESS_REASONS["NEW_HIRE"] = "New Hire";
BUSINESS_REASONS["NEW_ROLE"] = "New Role";
BUSINESS_REASONS["USER_MOVE"] = "User Move";
BUSINESS_REASONS["TRANSFER"] = "Transfer";
BUSINESS_REASONS["OTHER_REASON"] = "Other Reason";
})(BUSINESS_REASONS || (BUSINESS_REASONS = {}));
export class CartStore {
baseStore;
items = {};
targetUser = undefined;
businessReason = undefined;
initState = ActionState.create();
loadingState = ActionState.create();
submitState = ActionState.create();
open = false;
cartSummary = {
total_items: 0,
total_cost: 0,
formatted_total_cost: '$0.00',
};
constructor(baseStore) {
makeObservable(this, {
items: observable,
targetUser: observable,
businessReason: observable,
open: observable,
cartSummary: observable,
cartUser: computed,
cartItemIds: computed,
setOpen: action,
setTargetUser: flow,
setBusinessReason: action,
initialize: flow,
submitOrder: flow,
refresh: flow,
clearCart: flow,
deleteCartItem: flow,
addToCartWithAPI: flow,
addOrderProfileItemsToCart: flow,
});
this.baseStore = baseStore;
}
get currentUser() {
return this.baseStore.applicationStore.identityService.currentUser;
}
get cartUser() {
return this.targetUser ?? this.currentUser;
}
get cartItemIds() {
const ids = new Set();
for (const vendorProfileId in this.items) {
if (Object.prototype.hasOwnProperty.call(this.items, vendorProfileId)) {
const cartItems = this.items[Number(vendorProfileId)];
if (cartItems) {
for (const item of cartItems) {
ids.add(item.id);
}
}
}
}
return ids;
}
setOpen(val) {
this.open = val;
}
*setTargetUser(val) {
this.loadingState.inProgress();
this.targetUser = val;
this.items = {};
this.cartSummary = {
total_items: 0,
total_cost: 0,
formatted_total_cost: '$0.00',
};
this.businessReason = undefined;
try {
yield flowResult(this.refresh());
this.loadingState.complete();
}
catch (error) {
assertErrorThrown(error);
this.baseStore.applicationStore.logService.error(LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE), `Failed to load cart for user: ${error.message}`);
this.loadingState.fail();
}
}
setBusinessReason(val) {
this.businessReason = val;
}
isItemInCart(itemId) {
return this.cartItemIds.has(itemId);
}
/**
* Returns the add-on items that depend on the given cart item.
* When a Terminal is deleted, its associated add-ons (same vendor) must also be removed.
*/
getDependentAddOns(cartId) {
for (const vendorProfileId in this.items) {
if (Object.prototype.hasOwnProperty.call(this.items, vendorProfileId)) {
const cartItems = this.items[Number(vendorProfileId)];
if (cartItems) {
const target = cartItems.find((item) => item.cartId === cartId);
if (target && target.category === TerminalItemType.TERMINAL) {
return cartItems.filter((item) => item.cartId !== cartId &&
item.category === TerminalItemType.ADD_ON);
}
}
}
}
return [];
}
*addToCartWithAPI(cartItemData, suppressSuccessToast = false) {
const user = this.cartUser;
if (!user) {
const message = 'User not authenticated';
toastManager.error(message);
return { success: false, message };
}
this.loadingState.inProgress();
try {
const response = (yield this.baseStore.marketplaceServerClient.addToCart(user, cartItemData));
yield flowResult(this.refresh());
const responseMessage = response.message;
if (!/^2\d\d$/.test(String(response.status_code))) {
toastManager.warning(responseMessage);
}
else if (!suppressSuccessToast) {
toastManager.success(responseMessage);
}
const recommendations = response.marketplace_addons ?? response.marketplace_terminals ?? [];
const parentVendorId = response.vendor_profile_id;
if (parentVendorId && recommendations.length > 0) {
recommendations.forEach((item) => {
if (!item.vendorProfileId) {
item.vendorProfileId = parentVendorId;
}
if (item.skipWorkflow === undefined) {
item.skipWorkflow = true;
}
});
}
this.loadingState.complete();
return {
success: true,
recommendations,
message: responseMessage,
totalCount: response.total_count,
};
}
catch (error) {
assertErrorThrown(error);
const message = `Failed to add ${cartItemData.productName} to cart: ${error.message}`;
toastManager.error(message);
this.loadingState.fail();
return { success: false, message };
}
}
/**
* Adds a list of order-profile items to the cart, skipping already-owned ones.
* Each item is added sequentially so that vendor-profile items can be added
* before their associated add-ons.
*/
*addOrderProfileItemsToCart(items, suppressSuccessToast = false) {
for (const item of items) {
if (item.isOwned) {
continue;
}
yield flowResult(this.addToCartWithAPI({
id: item.id,
productName: item.productName,
providerName: item.providerName,
category: item.category,
price: item.price,
description: item.description ?? '',
isOwned: boolToString(item.isOwned),
...(item.model === null || item.model === undefined
? {}
: { model: item.model }),
skipWorkflow: true,
...(item.isMandatory === undefined
? {}
: { isMandatory: item.isMandatory }),
...(item.vendorProfileId === undefined
? {}
: { vendorProfileId: item.vendorProfileId }),
...(item.permissionId === undefined
? {}
: { permissionId: item.permissionId }),
}, suppressSuccessToast));
}
}
/**
* Returns true when all non-owned items of the profile are present in the
* cart. For multiselect profiles, at least one complete terminal bundle
* (terminal + its associated add-ons) must be fully in the cart.
*/
isOrderProfileInCart(profile) {
const nonOwnedItems = profile.items.filter((item) => !item.isOwned);
const nonOwnedTerminals = nonOwnedItems.filter((item) => item.isTerminal);
if (profile.multiselect) {
return nonOwnedTerminals.some((terminal) => {
const selectedModel = terminal.model ?? null;
const bundleItems = [
terminal,
...profile.items.filter((item) => !item.isTerminal &&
!item.isOwned &&
(selectedModel === null || item.model === selectedModel)),
];
return bundleItems.every((item) => this.isItemInCart(item.id));
});
}
return (nonOwnedItems.length > 0 &&
nonOwnedItems.every((item) => this.isItemInCart(item.id)));
}
providerToCartRequest(provider) {
const isInventory = provider.source === RecommendationSource.INVENTORY;
return {
id: isInventory ? (provider.permissionId ?? provider.id) : provider.id,
productName: provider.productName,
providerName: provider.providerName,
category: provider.category,
price: provider.price,
description: provider.description,
isOwned: boolToString(provider.isOwned),
model: provider.model ?? provider.productName,
skipWorkflow: provider.skipWorkflow ?? false,
...(provider.vendorProfileId !== undefined && {
vendorProfileId: provider.vendorProfileId,
}),
...(provider.permissionId !== undefined && {
permissionId: provider.permissionId,
}),
...(provider.source !== undefined && {
source: provider.source,
}),
};
}
*initialize() {
if (!this.initState.isInInitialState) {
return;
}
this.initState.inProgress();
try {
yield flowResult(this.refresh());
this.initState.complete();
}
catch (error) {
assertErrorThrown(error);
this.baseStore.applicationStore.logService.warn(LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE), 'Cart initialization failed, using empty state');
this.initState.fail();
}
}
*refresh() {
const user = this.cartUser;
if (!user) {
return;
}
try {
this.items = (yield this.baseStore.marketplaceServerClient.getCart(user));
this.cartSummary =
(yield this.baseStore.marketplaceServerClient.getCartSummary(user));
}
catch (error) {
assertErrorThrown(error);
this.baseStore.applicationStore.logService.error(LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE), `Failed to refresh cart: ${error.message}`);
}
}
*submitOrder() {
if (!this.businessReason) {
toastManager.warning('Please select a business reason before submitting order');
return;
}
if (this.cartSummary.total_items === 0) {
toastManager.warning('Cart is empty - nothing to order');
return;
}
const user = this.currentUser;
if (!user) {
toastManager.error('User not authenticated');
return;
}
this.submitState.inProgress();
try {
const orderData = {
ordered_by: user,
kerberos: this.cartUser,
order_items: this.items,
business_justification: this.businessReason,
};
yield this.baseStore.marketplaceServerClient.submitOrder(user, orderData);
toastManager.notify('Order created successfully!', 'success');
yield flowResult(this.refresh());
this.setBusinessReason(undefined);
this.open = false;
this.submitState.complete();
}
catch (error) {
assertErrorThrown(error);
const message = `Failed to submit order: ${error.message}`;
toastManager.error(message);
this.submitState.fail();
}
}
*clearCart() {
const user = this.cartUser;
if (!user) {
toastManager.error('User not authenticated');
return;
}
this.loadingState.inProgress();
try {
yield this.baseStore.marketplaceServerClient.clearCart(user);
yield flowResult(this.refresh());
toastManager.success('Cart cleared successfully');
this.loadingState.complete();
}
catch (error) {
assertErrorThrown(error);
const message = `Failed to clear cart: ${error.message}`;
toastManager.error(message);
this.loadingState.fail();
}
}
*deleteCartItem(cartId, confirmDelete) {
const user = this.cartUser;
if (!user) {
toastManager.error('User not authenticated');
return;
}
this.loadingState.inProgress();
try {
yield this.baseStore.marketplaceServerClient.deleteCartItem(user, cartId, confirmDelete);
yield flowResult(this.refresh());
toastManager.success('Item removed successfully');
this.loadingState.complete();
}
catch (error) {
assertErrorThrown(error);
const message = `Failed to remove item: ${error.message}`;
toastManager.error(message);
this.loadingState.fail();
}
}
static BUSINESS_REASONS = BUSINESS_REASONS;
}
//# sourceMappingURL=CartStore.js.map