diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index afd3b79c0b8..00808b4b299 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -936,10 +936,6 @@ qx.Class.define("osparc.data.Resources", { method: "PUT", url: statics.API + "/wallets/{walletId}/auto-recharge" }, - purchases: { - method: "GET", - url: statics.API + "/wallets/{walletId}/licensed-items-purchases" - }, } }, /* @@ -1300,10 +1296,18 @@ qx.Class.define("osparc.data.Resources", { method: "GET", url: statics.API + "/catalog/licensed-items?offset={offset}&limit={limit}" }, + purchases: { + method: "GET", + url: statics.API + "/wallets/{walletId}/licensed-items-purchases?offset={offset}&limit={limit}" + }, purchase: { method: "POST", url: statics.API + "/catalog/licensed-items/{licensedItemId}:purchase" }, + checkouts: { + method: "GET", + url: statics.API + "/wallets/{walletId}/licensed-items-checkouts?offset={offset}&limit={limit}" + }, } } }; @@ -1353,11 +1357,10 @@ qx.Class.define("osparc.data.Resources", { res[endpoint](params.url || null, params.data || null); } - res.addListenerOnce(endpoint + "Success", e => { + const successCB = e => { const response = e.getRequest().getResponse(); const endpointDef = resourceDefinition.endpoints[endpoint]; const data = endpointDef.isJsonFile ? response : response.data; - const useCache = ("useCache" in endpointDef) ? endpointDef.useCache : resourceDefinition.useCache; // OM: Temporary solution until the quality object is better defined if (data && endpoint.includes("get") && ["studies", "templates"].includes(resource)) { if (Array.isArray(data)) { @@ -1368,6 +1371,8 @@ qx.Class.define("osparc.data.Resources", { osparc.metadata.Quality.attachQualityToObject(data); } } + + const useCache = ("useCache" in endpointDef) ? endpointDef.useCache : resourceDefinition.useCache; if (useCache) { if (endpoint.includes("delete") && resourceDefinition["deleteId"] && resourceDefinition["deleteId"] in params.url) { const deleteId = params.url[resourceDefinition["deleteId"]]; @@ -1382,16 +1387,18 @@ qx.Class.define("osparc.data.Resources", { } } } + res.dispose(); + if ("resolveWResponse" in options && options.resolveWResponse) { response.params = params; resolve(response); } else { resolve(data); } - }, this); + }; - res.addListener(endpoint + "Error", e => { + const errorCB = e => { if (e.getPhase() === "timeout") { if (options.timeout && options.timeoutRetries) { options.timeoutRetries--; @@ -1445,8 +1452,12 @@ qx.Class.define("osparc.data.Resources", { err.status = status; } reject(err); - }); + }; + const successEndpoint = endpoint + "Success"; + const errorEndpoint = endpoint + "Error"; + res.addListenerOnce(successEndpoint, e => successCB(e), this); + res.addListener(errorEndpoint, e => errorCB(e), this); sendRequest(); }); }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js b/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js index 8351877752c..8e68c41070c 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js @@ -33,6 +33,11 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", { if (osparc.data.Permissions.getInstance().canDo("usage.all.read")) { this.__usagePage = this.__addUsagePage(); } + + if (osparc.product.Utils.showS4LStore()) { + this.__addPurchasesPage(); + this.__addCheckoutsPage(); + } }, statics: { @@ -80,7 +85,7 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", { }, __addTransactionsPage: function() { - const title = this.tr("Transactions"); + const title = this.tr("Payments"); const iconSrc = "@FontAwesome5Solid/exchange-alt/22"; const transactions = this.__transactionsTable = new osparc.desktop.credits.Transactions(); const page = this.addTab(title, iconSrc, transactions); @@ -95,6 +100,22 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", { return page; }, + __addPurchasesPage: function() { + const title = this.tr("Purchases"); + const iconSrc = "@FontAwesome5Solid/list/22"; + const purchases = new osparc.desktop.credits.Purchases(); + const page = this.addTab(title, iconSrc, purchases); + return page; + }, + + __addCheckoutsPage: function() { + const title = this.tr("Checkouts"); + const iconSrc = "@FontAwesome5Solid/list/22"; + const purchases = new osparc.desktop.credits.Checkouts(); + const page = this.addTab(title, iconSrc, purchases); + return page; + }, + openWallets: function() { if (this.__walletsPage) { return this._openPage(this.__walletsPage); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Checkouts.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Checkouts.js new file mode 100644 index 00000000000..e2b7c891913 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Checkouts.js @@ -0,0 +1,48 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.desktop.credits.Checkouts", { + extend: osparc.desktop.credits.ResourceInTableViewer, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "table": { + const dateFilters = this.getChildControl("date-filters"); + control = new osparc.desktop.credits.CheckoutsTable(this._getSelectWalletId(), dateFilters.getValue()).set({ + marginTop: 10 + }); + const fetchingImage = this.getChildControl("fetching-image"); + control.getTableModel().bind("isFetching", fetchingImage, "visibility", { + converter: isFetching => isFetching ? "visible" : "excluded" + }) + this._add(control, { flex: 1 }) + break; + } + } + return control || this.base(arguments, id); + }, + + _buildLayout: function() { + this.base(arguments); + + this.getChildControl("export-button").exclude(); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CheckoutsTable.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CheckoutsTable.js new file mode 100644 index 00000000000..a757d6e3f2f --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CheckoutsTable.js @@ -0,0 +1,87 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.desktop.credits.CheckoutsTable", { + extend: qx.ui.table.Table, + + construct: function(walletId, filters) { + this.base(arguments); + + const model = new osparc.desktop.credits.CheckoutsTableModel(walletId, filters); + this.setTableModel(model); + + this.set({ + statusBarVisible: false, + headerCellHeight: 26, + rowHeight: 26, + }); + + const columnModel = this.getTableColumnModel(); + columnModel.setColumnVisible(this.self().COLS.CHECKOUT_ID.column, false); + columnModel.setColumnVisible(this.self().COLS.ITEM_ID.column, false); + + Object.values(this.self().COLS).forEach(col => columnModel.setColumnWidth(col.column, col.width)); + }, + + statics: { + COLS: { + CHECKOUT_ID: { + id: "checkoutId", + column: 0, + label: qx.locale.Manager.tr("CheckoutId"), + width: 150 + }, + ITEM_ID: { + id: "itemId", + column: 1, + label: qx.locale.Manager.tr("ItemId"), + width: 150 + }, + ITEM_LABEL: { + id: "itemLabel", + column: 2, + label: qx.locale.Manager.tr("Name"), + width: 150 + }, + START: { + id: "start", + column: 3, + label: qx.locale.Manager.tr("Start"), + width: 150 + }, + DURATION: { + id: "duration", + column: 4, + label: qx.locale.Manager.tr("Duration"), + width: 150 + }, + SEATS: { + id: "seats", + column: 5, + label: qx.locale.Manager.tr("Seats"), + width: 50 + }, + USER: { + id: "user", + column: 6, + label: qx.locale.Manager.tr("User"), + width: 100 + }, + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CheckoutsTableModel.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CheckoutsTableModel.js new file mode 100644 index 00000000000..fe59f001d04 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CheckoutsTableModel.js @@ -0,0 +1,186 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.desktop.credits.CheckoutsTableModel", { + extend: qx.ui.table.model.Remote, + + construct(walletId, filters) { + this.base(arguments); + + const checkoutsCols = osparc.desktop.credits.CheckoutsTable.COLS; + const colLabels = Object.values(checkoutsCols).map(col => col.label); + const colIDs = Object.values(checkoutsCols).map(col => col.id); + + this.setColumns(colLabels, colIDs); + this.setWalletId(walletId); + if (filters) { + this.setFilters(filters); + } + this.setSortColumnIndexWithoutSortingData(checkoutsCols.START.column); + this.setSortAscendingWithoutSortingData(false); + this.setColumnSortable(checkoutsCols.DURATION.column, false); + }, + + properties: { + walletId: { + check: "Number", + nullable: true + }, + + filters: { + check: "Object", + init: null + }, + + isFetching: { + check: "Boolean", + init: false, + event: "changeFetching" + }, + + orderBy: { + check: "Object", + init: { + field: "startAt", + direction: "desc" + } + }, + }, + + statics: { + SERVER_MAX_LIMIT: 49, + COLUMN_ID_TO_DB_COLUMN_MAP: { + 0: "startAt", + }, + }, + + members: { + // overridden + sortByColumn(columnIndex, ascending) { + this.setOrderBy({ + field: this.self().COLUMN_ID_TO_DB_COLUMN_MAP[columnIndex], + direction: ascending ? "asc" : "desc" + }) + this.base(arguments, columnIndex, ascending); + }, + + // overridden + _loadRowCount() { + const walletId = this.getWalletId(); + const urlParams = { + offset: 0, + limit: 1, + filters: this.getFilters() ? + JSON.stringify({ + "startAt": this.getFilters() + }) : + null, + orderBy: JSON.stringify(this.getOrderBy()), + }; + const options = { + resolveWResponse: true + }; + osparc.store.LicensedItems.getInstance().getCheckedOutLicensedItems(walletId, urlParams, options) + .then(resp => this._onRowCountLoaded(resp["_meta"].total)) + .catch(() => this._onRowCountLoaded(null)); + }, + + // overridden + _loadRowData(firstRow, qxLastRow) { + this.setIsFetching(true); + + const lastRow = Math.min(qxLastRow, this._rowCount - 1); + // Returns a request promise with given offset and limit + const getFetchPromise = (offset, limit=this.self().SERVER_MAX_LIMIT) => { + const walletId = this.getWalletId(); + const urlParams = { + limit, + offset, + filters: this.getFilters() ? + JSON.stringify({ + "started_at": this.getFilters() + }) : + null, + orderBy: JSON.stringify(this.getOrderBy()) + }; + const licensedItemsStore = osparc.store.LicensedItems.getInstance(); + return Promise.all([ + licensedItemsStore.getLicensedItems(), + licensedItemsStore.getCheckedOutLicensedItems(walletId, urlParams), + licensedItemsStore.getVipModels(), + ]) + .then(values => { + const licensedItems = values[0]; + const checkoutsItems = values[1]; + const vipModels = values[2]; + + const data = []; + const checkoutsCols = osparc.desktop.credits.CheckoutsTable.COLS; + checkoutsItems.forEach(checkoutsItem => { + const licensedItemId = checkoutsItem["licensedItemId"]; + const licensedItem = licensedItems.find(licItem => licItem["licensedItemId"] === licensedItemId); + const vipModel = vipModels.find(vipMdl => licensedItem && (vipMdl["modelId"] == licensedItem["name"])); + let start = ""; + let duration = ""; + if (checkoutsItem["startedAt"]) { + start = osparc.utils.Utils.formatDateAndTime(new Date(checkoutsItem["startedAt"])); + if (checkoutsItem["stoppedAt"]) { + duration = osparc.utils.Utils.formatMsToHHMMSS(new Date(checkoutsItem["stoppedAt"]) - new Date(checkoutsItem["startedAt"])); + } + } + data.push({ + [checkoutsCols.CHECKOUT_ID.id]: checkoutsItem["licensedItemCheckoutId"], + [checkoutsCols.ITEM_ID.id]: licensedItemId, + [checkoutsCols.ITEM_LABEL.id]: vipModel ? vipModel["name"] : "unknown model", + [checkoutsCols.START.id]: start, + [checkoutsCols.DURATION.id]: duration, + [checkoutsCols.SEATS.id]: checkoutsItem["numOfSeats"], + [checkoutsCols.USER.id]: checkoutsItem["userId"], + }); + }); + return data; + }); + }; + + // Divides the model row request into several server requests to comply with the number of rows server limit + const reqLimit = lastRow - firstRow + 1; // Number of requested rows + const nRequests = Math.ceil(reqLimit / this.self().SERVER_MAX_LIMIT); + if (nRequests > 1) { + const requests = []; + for (let i=firstRow; i <= lastRow; i += this.self().SERVER_MAX_LIMIT) { + requests.push(getFetchPromise(i, i > lastRow - this.self().SERVER_MAX_LIMIT + 1 ? reqLimit % this.self().SERVER_MAX_LIMIT : this.self().SERVER_MAX_LIMIT)) + } + Promise.all(requests) + .then(responses => this._onRowDataLoaded(responses.flat())) + .catch(err => { + console.error(err); + this._onRowDataLoaded(null); + }) + .finally(() => this.setIsFetching(false)); + } else { + getFetchPromise(firstRow, reqLimit) + .then(data => this._onRowDataLoaded(data)) + .catch(err => { + console.error(err) + this._onRowDataLoaded(null); + }) + .finally(() => this.setIsFetching(false)); + } + } + } +}) diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Purchases.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Purchases.js new file mode 100644 index 00000000000..fdc2640621b --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Purchases.js @@ -0,0 +1,48 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.desktop.credits.Purchases", { + extend: osparc.desktop.credits.ResourceInTableViewer, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "table": { + const dateFilters = this.getChildControl("date-filters"); + control = new osparc.desktop.credits.PurchasesTable(this._getSelectWalletId(), dateFilters.getValue()).set({ + marginTop: 10 + }); + const fetchingImage = this.getChildControl("fetching-image"); + control.getTableModel().bind("isFetching", fetchingImage, "visibility", { + converter: isFetching => isFetching ? "visible" : "excluded" + }) + this._add(control, { flex: 1 }) + break; + } + } + return control || this.base(arguments, id); + }, + + _buildLayout: function() { + this.base(arguments); + + this.getChildControl("export-button").exclude(); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/PurchasesTable.js b/services/static-webserver/client/source/class/osparc/desktop/credits/PurchasesTable.js new file mode 100644 index 00000000000..8cd31c0287c --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/PurchasesTable.js @@ -0,0 +1,94 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.desktop.credits.PurchasesTable", { + extend: qx.ui.table.Table, + + construct: function(walletId, filters) { + this.base(arguments); + + const model = new osparc.desktop.credits.PurchasesTableModel(walletId, filters); + this.setTableModel(model); + + this.set({ + statusBarVisible: false, + headerCellHeight: 26, + rowHeight: 26, + }); + + const columnModel = this.getTableColumnModel(); + columnModel.setColumnVisible(this.self().COLS.PURCHASE_ID.column, false); + columnModel.setColumnVisible(this.self().COLS.ITEM_ID.column, false); + columnModel.setDataCellRenderer(this.self().COLS.COST.column, new qx.ui.table.cellrenderer.Number()); + + Object.values(this.self().COLS).forEach(col => columnModel.setColumnWidth(col.column, col.width)); + }, + + statics: { + COLS: { + PURCHASE_ID: { + id: "purchaseId", + column: 0, + label: qx.locale.Manager.tr("PurchaseId"), + width: 150 + }, + ITEM_ID: { + id: "itemId", + column: 1, + label: qx.locale.Manager.tr("ItemId"), + width: 150 + }, + ITEM_LABEL: { + id: "itemLabel", + column: 2, + label: qx.locale.Manager.tr("Name"), + width: 150 + }, + START: { + id: "start", + column: 3, + label: qx.locale.Manager.tr("Start"), + width: 150 + }, + END: { + id: "end", + column: 4, + label: qx.locale.Manager.tr("End"), + width: 150 + }, + SEATS: { + id: "seats", + column: 5, + label: qx.locale.Manager.tr("Seats"), + width: 50 + }, + COST: { + id: "cost", + column: 6, + label: qx.locale.Manager.tr("Credits"), + width: 60 + }, + USER: { + id: "user", + column: 7, + label: qx.locale.Manager.tr("User"), + width: 100 + }, + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/PurchasesTableModel.js b/services/static-webserver/client/source/class/osparc/desktop/credits/PurchasesTableModel.js new file mode 100644 index 00000000000..b1b054071ce --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/PurchasesTableModel.js @@ -0,0 +1,178 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.desktop.credits.PurchasesTableModel", { + extend: qx.ui.table.model.Remote, + + construct(walletId, filters) { + this.base(arguments); + + const purchasesCols = osparc.desktop.credits.PurchasesTable.COLS; + const colLabels = Object.values(purchasesCols).map(col => col.label); + const colIDs = Object.values(purchasesCols).map(col => col.id); + + this.setColumns(colLabels, colIDs); + this.setWalletId(walletId); + if (filters) { + this.setFilters(filters); + } + this.setSortColumnIndexWithoutSortingData(purchasesCols.START.column); + this.setSortAscendingWithoutSortingData(false); + }, + + properties: { + walletId: { + check: "Number", + nullable: true + }, + + filters: { + check: "Object", + init: null + }, + + isFetching: { + check: "Boolean", + init: false, + event: "changeFetching" + }, + + orderBy: { + check: "Object", + init: { + field: "startAt", + direction: "desc" + } + }, + }, + + statics: { + SERVER_MAX_LIMIT: 49, + COLUMN_ID_TO_DB_COLUMN_MAP: { + 0: "startAt", + }, + }, + + members: { + // overridden + sortByColumn(columnIndex, ascending) { + this.setOrderBy({ + field: this.self().COLUMN_ID_TO_DB_COLUMN_MAP[columnIndex], + direction: ascending ? "asc" : "desc" + }) + this.base(arguments, columnIndex, ascending); + }, + + // overridden + _loadRowCount() { + const walletId = this.getWalletId(); + const urlParams = { + offset: 0, + limit: 1, + filters: this.getFilters() ? + JSON.stringify({ + "startAt": this.getFilters() + }) : + null, + orderBy: JSON.stringify(this.getOrderBy()), + }; + const options = { + resolveWResponse: true + }; + osparc.store.LicensedItems.getInstance().getPurchasedLicensedItems(walletId, urlParams, options) + .then(resp => this._onRowCountLoaded(resp["_meta"].total)) + .catch(() => this._onRowCountLoaded(null)); + }, + + // overridden + _loadRowData(firstRow, qxLastRow) { + this.setIsFetching(true); + + const lastRow = Math.min(qxLastRow, this._rowCount - 1); + // Returns a request promise with given offset and limit + const getFetchPromise = (offset, limit=this.self().SERVER_MAX_LIMIT) => { + const walletId = this.getWalletId(); + const urlParams = { + limit, + offset, + filters: this.getFilters() ? + JSON.stringify({ + "started_at": this.getFilters() + }) : + null, + orderBy: JSON.stringify(this.getOrderBy()) + }; + const licensedItemsStore = osparc.store.LicensedItems.getInstance(); + return Promise.all([ + licensedItemsStore.getLicensedItems(), + licensedItemsStore.getPurchasedLicensedItems(walletId, urlParams), + licensedItemsStore.getVipModels(), + ]) + .then(values => { + const licensedItems = values[0]; + const purchasesItems = values[1]; + const vipModels = values[2]; + + const data = []; + const purchasesCols = osparc.desktop.credits.PurchasesTable.COLS; + purchasesItems.forEach(purchasesItem => { + const licensedItemId = purchasesItem["licensedItemId"]; + const licensedItem = licensedItems.find(licItem => licItem["licensedItemId"] === licensedItemId); + const vipModel = vipModels.find(vipMdl => licensedItem && (vipMdl["modelId"] == licensedItem["name"])); + data.push({ + [purchasesCols.PURCHASE_ID.id]: purchasesItem["licensedItemPurchaseId"], + [purchasesCols.ITEM_ID.id]: licensedItemId, + [purchasesCols.ITEM_LABEL.id]: vipModel ? vipModel["name"] : "unknown model", + [purchasesCols.START.id]: osparc.utils.Utils.formatDateAndTime(new Date(purchasesItem["startAt"])), + [purchasesCols.END.id]: osparc.utils.Utils.formatDateAndTime(new Date(purchasesItem["expireAt"])), + [purchasesCols.SEATS.id]: purchasesItem["numOfSeats"], + [purchasesCols.COST.id]: purchasesItem["pricingUnitCost"] ? ("-" + parseFloat(purchasesItem["pricingUnitCost"]).toFixed(2)) : "", // show it negative + [purchasesCols.USER.id]: purchasesItem["purchasedByUser"], + }); + }); + return data; + }); + }; + + // Divides the model row request into several server requests to comply with the number of rows server limit + const reqLimit = lastRow - firstRow + 1; // Number of requested rows + const nRequests = Math.ceil(reqLimit / this.self().SERVER_MAX_LIMIT); + if (nRequests > 1) { + const requests = []; + for (let i=firstRow; i <= lastRow; i += this.self().SERVER_MAX_LIMIT) { + requests.push(getFetchPromise(i, i > lastRow - this.self().SERVER_MAX_LIMIT + 1 ? reqLimit % this.self().SERVER_MAX_LIMIT : this.self().SERVER_MAX_LIMIT)) + } + Promise.all(requests) + .then(responses => this._onRowDataLoaded(responses.flat())) + .catch(err => { + console.error(err); + this._onRowDataLoaded(null); + }) + .finally(() => this.setIsFetching(false)); + } else { + getFetchPromise(firstRow, reqLimit) + .then(data => this._onRowDataLoaded(data)) + .catch(err => { + console.error(err) + this._onRowDataLoaded(null); + }) + .finally(() => this.setIsFetching(false)); + } + } + } +}) diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/ResourceInTableViewer.js b/services/static-webserver/client/source/class/osparc/desktop/credits/ResourceInTableViewer.js new file mode 100644 index 00000000000..baf94205c9b --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/ResourceInTableViewer.js @@ -0,0 +1,150 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + * Ignacio Pascual (ignapas) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.ResourceInTableViewer", { + extend: qx.ui.core.Widget, + type: "abstract", + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(5)); + + this._buildLayout(); + }, + + members: { + _createChildControlImpl: function(id) { + let layout; + let control; + switch (id) { + case "intro-text": + control = new qx.ui.basic.Label("Select a Credit Account:"); + this._add(control); + break; + case "wallet-selector-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + this._add(control); + break; + case "wallet-selector": + control = new qx.ui.form.SelectBox().set({ + allowStretchX: false, + width: 200 + }); + layout = this.getChildControl("wallet-selector-layout"); + layout.add(control); + break; + case "fetching-image": + control = new qx.ui.basic.Image().set({ + source: "@FontAwesome5Solid/circle-notch/12", + alignX: "center", + alignY: "middle", + visibility: "excluded" + }); + control.getContentElement().addClass("rotate"); + layout = this.getChildControl("wallet-selector-layout"); + layout.add(control); + break; + case "filter-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + this._add(control); + break; + case "date-filters": + control = new osparc.desktop.credits.DateFilters(); + control.addListener("change", e => { + const table = this.getChildControl("table"); + table.getTableModel().setFilters(e.getData()); + table.getTableModel().reloadData(); + }); + layout = this.getChildControl("filter-layout"); + layout.add(control); + break; + case "export-button": + control = new qx.ui.form.Button(this.tr("Export")).set({ + allowStretchY: false, + alignY: "bottom", + }); + control.addListener("execute", () => this._handleExport()); + layout = this.getChildControl("filter-layout"); + layout.add(control); + break; + case "reload-button": + control = new qx.ui.form.Button(this.tr("Reload"), "@FontAwesome5Solid/sync-alt/14").set({ + allowStretchY: false, + alignY: "bottom" + }); + control.addListener("execute", () => { + const table = this.getChildControl("table"); + table.getTableModel().reloadData(); + }); + layout = this.getChildControl("filter-layout"); + layout.add(control); + break; + } + return control || this.base(arguments, id); + }, + + _buildLayout: function() { + const introText = this.getChildControl("intro-text"); + const walletSelectBox = this.getChildControl("wallet-selector"); + this.getChildControl("fetching-image"); + + const filterLayout = this.getChildControl("filter-layout"); + this.getChildControl("date-filters"); + filterLayout.add(new qx.ui.core.Spacer(), { + flex: 1 + }); + const exportButton = this.getChildControl("export-button"); + this.getChildControl("reload-button"); + + walletSelectBox.addListener("changeSelection", e => { + const selection = e.getData(); + if (selection.length) { + const table = this.getChildControl("table"); + table.getTableModel().setWalletId(this._getSelectWalletId()); + table.getTableModel().reloadData(); + } + }); + + if (osparc.desktop.credits.Utils.areWalletsEnabled()) { + const store = osparc.store.Store.getInstance(); + store.getWallets().forEach(wallet => { + walletSelectBox.add(new qx.ui.form.ListItem(wallet.getName(), null, wallet)); + }); + } else { + introText.setVisibility("excluded"); + walletSelectBox.setVisibility("excluded"); + exportButton.setVisibility("excluded"); + this.getChildControl("table"); + } + }, + + _getSelectWalletId: function() { + if (osparc.desktop.credits.Utils.areWalletsEnabled()) { + const walletSelectBox = this.getChildControl("wallet-selector"); + const selectedWallet = walletSelectBox.getSelection()[0].getModel(); + return selectedWallet.getWalletId(); + } + return null; + }, + + _handleExport: function() { + throw new Error("Abstract method called!"); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js index 8f4eaf1075c..99af443b8e3 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js @@ -17,114 +17,34 @@ ************************************************************************ */ qx.Class.define("osparc.desktop.credits.Usage", { - extend: qx.ui.core.Widget, - - construct: function() { - this.base(arguments); - - this._setLayout(new qx.ui.layout.VBox(15)); - - const store = osparc.store.Store.getInstance(); - this.__userWallets = store.getWallets(); - - this.__buildLayout() - }, + extend: osparc.desktop.credits.ResourceInTableViewer, members: { - __buildLayout: function() { - this._removeAll(); - - const container = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)); - - const lbl = new qx.ui.basic.Label("Select a Credit Account:"); - container.add(lbl); - - const selectBoxContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); - const walletSelectBox = new qx.ui.form.SelectBox().set({ - allowStretchX: false, - width: 200 - }); - selectBoxContainer.add(walletSelectBox); - this.__fetchingImg = new qx.ui.basic.Image().set({ - source: "@FontAwesome5Solid/circle-notch/12", - alignX: "center", - alignY: "middle", - visibility: "excluded" - }); - this.__fetchingImg.getContentElement().addClass("rotate"); - selectBoxContainer.add(this.__fetchingImg); - container.add(selectBoxContainer); - - const filterContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)) - this.__dateFilters = new osparc.desktop.credits.DateFilters(); - this.__dateFilters.addListener("change", e => { - this.__table.getTableModel().setFilters(e.getData()) - this.__table.getTableModel().reloadData() - }); - filterContainer.add(this.__dateFilters); - filterContainer.add(new qx.ui.core.Spacer(), { - flex: 1 - }); - this.__exportButton = new qx.ui.form.Button(this.tr("Export")).set({ - allowStretchY: false, - alignY: "bottom" - }); - this.__exportButton.addListener("execute", () => { - this.__handleExport() - }); - filterContainer.add(this.__exportButton); - const refreshButton = new qx.ui.form.Button(this.tr("Reload"), "@FontAwesome5Solid/sync-alt/14").set({ - allowStretchY: false, - alignY: "bottom" - }); - refreshButton.addListener("execute", () => this.__table && this.__table.getTableModel().reloadData()); - filterContainer.add(refreshButton) - container.add(filterContainer); - - this._add(container); - - walletSelectBox.addListener("changeSelection", e => { - const selection = e.getData(); - if (selection.length) { - this.__selectedWallet = selection[0].getModel() - if (this.__table) { - this.__table.getTableModel().setWalletId(this.__selectedWallet.getWalletId()) - this.__table.getTableModel().reloadData() - } else { - // qx: changeSelection is triggered after the first item is added to SelectBox - this.__table = new osparc.desktop.credits.UsageTable(this.__selectedWallet.getWalletId(), this.__dateFilters.getValue()).set({ - marginTop: 10 - }) - this.__table.getTableModel().bind("isFetching", this.__fetchingImg, "visibility", { - converter: isFetching => isFetching ? "visible" : "excluded" - }) - container.add(this.__table, { flex: 1 }) - } + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "table": { + const dateFilters = this.getChildControl("date-filters"); + control = new osparc.desktop.credits.UsageTable(this._getSelectWalletId(), dateFilters.getValue()).set({ + marginTop: 10 + }); + const fetchingImage = this.getChildControl("fetching-image"); + control.getTableModel().bind("isFetching", fetchingImage, "visibility", { + converter: isFetching => isFetching ? "visible" : "excluded" + }) + this._add(control, { flex: 1 }) + break; } - }); - - if (osparc.desktop.credits.Utils.areWalletsEnabled()) { - this.__userWallets.forEach(wallet => { - walletSelectBox.add(new qx.ui.form.ListItem(wallet.getName(), null, wallet)); - }); - } else { - lbl.setVisibility("excluded") - walletSelectBox.setVisibility("excluded") - this.__exportButton.setVisibility("excluded") - this.__table = new osparc.desktop.credits.UsageTable(null, this.__dateFilters.getValue()).set({ - marginTop: 10 - }) - this.__table.getTableModel().bind("isFetching", this.__fetchingImg, "visibility", { - converter: isFetching => isFetching ? "visible" : "excluded" - }) - container.add(this.__table, { flex: 1 }) } + return control || this.base(arguments, id); + }, + + _handleExport: function() { + const reportUrl = new URL("/v0/services/-/usage-report", window.location.origin); + reportUrl.searchParams.append("wallet_id", this._getSelectWalletId()); + const dateFilters = this.getChildControl("date-filters"); + reportUrl.searchParams.append("filters", JSON.stringify({ "started_at": dateFilters.getValue() })); + window.open(reportUrl, "_blank"); }, - __handleExport() { - const reportUrl = new URL("/v0/services/-/usage-report", window.location.origin) - reportUrl.searchParams.append("wallet_id", this.__selectedWallet.getWalletId()) - reportUrl.searchParams.append("filters", JSON.stringify({ "started_at": this.__dateFilters.getValue() })) - window.open(reportUrl, "_blank") - } } }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js b/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js index 324f4dcb419..d08615d1a04 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js @@ -77,7 +77,7 @@ qx.Class.define("osparc.desktop.credits.UsageTable", { id: "node", column: 1, label: qx.locale.Manager.tr("Node"), - width: 140 + width: 100 }, SERVICE: { id: "service", @@ -119,7 +119,7 @@ qx.Class.define("osparc.desktop.credits.UsageTable", { id: "tags", column: 7, label: qx.locale.Manager.tr("Tags"), - width: 140 + width: 80 }, } } diff --git a/services/static-webserver/client/source/class/osparc/file/FileLabelWithActions.js b/services/static-webserver/client/source/class/osparc/file/FileLabelWithActions.js index 93df9367450..43113f880e2 100644 --- a/services/static-webserver/client/source/class/osparc/file/FileLabelWithActions.js +++ b/services/static-webserver/client/source/class/osparc/file/FileLabelWithActions.js @@ -183,7 +183,7 @@ qx.Class.define("osparc.file.FileLabelWithActions", { .then(datas => { if (datas.length) { this.fireDataEvent("fileDeleted", datas[0]); - osparc.FlashMessenger.getInstance().logAs(this.tr("Files successfully deleted"), "ERROR"); + osparc.FlashMessenger.getInstance().logAs(this.tr("Files successfully deleted"), "INFO"); } }); requests diff --git a/services/static-webserver/client/source/class/osparc/store/LicensedItems.js b/services/static-webserver/client/source/class/osparc/store/LicensedItems.js new file mode 100644 index 00000000000..7e86d835d98 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/store/LicensedItems.js @@ -0,0 +1,165 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.store.LicensedItems", { + extend: qx.core.Object, + type: "singleton", + + construct: function() { + this.base(arguments); + + this.__licensedItems = null; + this.__modelsCache = {}; + }, + + statics: { + VIP_MODELS: { + HUMAN_BODY: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/HumanWholeBody", + HUMAN_BODY_REGION: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/HumanBodyRegion", + ANIMAL: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/AnimalWholeBody", + PHANTOM: "https://speag.swiss/PD_DirectDownload/getDownloadableItems/ComputationalPhantom", + }, + + curateAnatomicalModels: function(anatomicalModelsRaw) { + const anatomicalModels = []; + const models = anatomicalModelsRaw["availableDownloads"]; + models.forEach(model => { + const curatedModel = {}; + Object.keys(model).forEach(key => { + if (key === "Features") { + let featuresRaw = model["Features"]; + featuresRaw = featuresRaw.substring(1, featuresRaw.length-1); // remove brackets + featuresRaw = featuresRaw.split(","); // split the string by commas + const features = {}; + featuresRaw.forEach(pair => { // each pair is "key: value" + const keyValue = pair.split(":"); + features[keyValue[0].trim()] = keyValue[1].trim() + }); + curatedModel["Features"] = features; + } else { + curatedModel[key] = model[key]; + } + }); + anatomicalModels.push(curatedModel); + }); + return anatomicalModels; + }, + }, + + members: { + __licensedItems: null, + __modelsCache: null, + + getLicensedItems: function() { + if (this.__licensedItems) { + return new Promise(resolve => resolve(this.__licensedItems)); + } + + return osparc.data.Resources.getInstance().getAllPages("licensedItems") + .then(licensedItems => { + this.__licensedItems = licensedItems; + return this.__licensedItems; + }); + }, + + getPurchasedLicensedItems: function(walletId, urlParams, options = {}) { + let purchasesParams = { + url: { + walletId, + offset: 0, + limit: 49, + } + }; + if (urlParams) { + purchasesParams.url = Object.assign(purchasesParams.url, urlParams); + } + return osparc.data.Resources.fetch("licensedItems", "purchases", purchasesParams, options); + }, + + purchaseLicensedItem: function(licensedItemId, walletId, pricingPlanId, pricingUnitId, numberOfSeats) { + const params = { + url: { + licensedItemId + }, + data: { + "wallet_id": walletId, + "pricing_plan_id": pricingPlanId, + "pricing_unit_id": pricingUnitId, + "num_of_seats": numberOfSeats, // this should go away + }, + } + return osparc.data.Resources.fetch("licensedItems", "purchase", params); + }, + + getCheckedOutLicensedItems: function(walletId, urlParams, options = {}) { + let purchasesParams = { + url: { + walletId, + offset: 0, + limit: 49, + } + }; + if (urlParams) { + purchasesParams.url = Object.assign(purchasesParams.url, urlParams); + } + return osparc.data.Resources.fetch("licensedItems", "checkouts", purchasesParams, options); + }, + + __fetchVipModels: async function(vipSubset) { + if (!(vipSubset in this.self().VIP_MODELS)) { + return []; + } + + if (vipSubset in this.__modelsCache) { + return this.__modelsCache[vipSubset]; + } + + return await fetch(this.self().VIP_MODELS[vipSubset], { + method:"POST" + }) + .then(resp => resp.json()) + .then(anatomicalModelsRaw => { + const allAnatomicalModels = this.self().curateAnatomicalModels(anatomicalModelsRaw); + const anatomicalModels = []; + allAnatomicalModels.forEach(model => { + const anatomicalModel = {}; + anatomicalModel["modelId"] = model["ID"]; + anatomicalModel["thumbnail"] = model["Thumbnail"]; + anatomicalModel["name"] = model["Features"]["name"] + " " + model["Features"]["version"]; + anatomicalModel["description"] = model["Description"]; + anatomicalModel["features"] = model["Features"]; + anatomicalModel["date"] = model["Features"]["date"]; + anatomicalModel["DOI"] = model["DOI"]; + anatomicalModels.push(anatomicalModel); + }); + this.__modelsCache[vipSubset] = anatomicalModels; + return anatomicalModels; + }); + }, + + getVipModels: async function(vipSubset) { + const vipModels = this.self().VIP_MODELS; + if (vipSubset && vipSubset in vipModels) { + return await this.__fetchVipModels(vipSubset); + } + const promises = []; + Object.keys(vipModels).forEach(sbs => promises.push(this.__fetchVipModels(sbs))); + return await Promise.all(promises) + .then(values => values.flat()); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/Market.js b/services/static-webserver/client/source/class/osparc/vipMarket/Market.js index dd4f9567f4d..2d88ab85ad8 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/Market.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/Market.js @@ -27,28 +27,28 @@ qx.Class.define("osparc.vipMarket.Market", { }); this.addWidgetOnTopOfTheTabs(miniWallet); - osparc.data.Resources.getInstance().getAllPages("licensedItems") + osparc.store.LicensedItems.getInstance().getLicensedItems() .then(() => { [{ category: "human", label: "Humans", icon: "@FontAwesome5Solid/users/20", - url: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/HumanWholeBody", + vipSubset: "HUMAN_BODY", }, { category: "human_region", label: "Humans (Region)", icon: "@FontAwesome5Solid/users/20", - url: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/HumanBodyRegion", + vipSubset: "HUMAN_BODY_REGION", }, { category: "animal", label: "Animals", icon: "@FontAwesome5Solid/users/20", - url: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/AnimalWholeBody", + vipSubset: "ANIMAL", }, { category: "phantom", label: "Phantoms", icon: "@FontAwesome5Solid/users/20", - url: "https://speag.swiss/PD_DirectDownload/getDownloadableItems/ComputationalPhantom", + vipSubset: "PHANTOM", }].forEach(marketInfo => { this.__buildViPMarketPage(marketInfo); }); @@ -76,7 +76,7 @@ qx.Class.define("osparc.vipMarket.Market", { __buildViPMarketPage: function(marketInfo) { const vipMarketView = new osparc.vipMarket.VipMarket(); vipMarketView.set({ - metadataUrl: marketInfo["url"], + vipSubset: marketInfo["vipSubset"], }); this.bind("openBy", vipMarketView, "openBy"); vipMarketView.addListener("importMessageSent", () => this.fireEvent("importMessageSent")); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js b/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js index 79b2626f260..6ff8e1b71d0 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js @@ -38,38 +38,11 @@ qx.Class.define("osparc.vipMarket.VipMarket", { event: "changeOpenBy", }, - metadataUrl: { - check: "String", + vipSubset: { + check: ["HUMAN_BODY", "HUMAN_BODY_REGION", "ANIMAL", "PHANTOM"], init: null, - nullable: false, + nullable: true, apply: "__fetchModels", - } - }, - - statics: { - curateAnatomicalModels: function(anatomicalModelsRaw) { - const anatomicalModels = []; - const models = anatomicalModelsRaw["availableDownloads"]; - models.forEach(model => { - const curatedModel = {}; - Object.keys(model).forEach(key => { - if (key === "Features") { - let featuresRaw = model["Features"]; - featuresRaw = featuresRaw.substring(1, featuresRaw.length-1); // remove brackets - featuresRaw = featuresRaw.split(","); // split the string by commas - const features = {}; - featuresRaw.forEach(pair => { // each pair is "key: value" - const keyValue = pair.split(":"); - features[keyValue[0].trim()] = keyValue[1].trim() - }); - curatedModel["Features"] = features; - } else { - curatedModel[key] = model[key]; - } - }); - anatomicalModels.push(curatedModel); - }); - return anatomicalModels; }, }, @@ -192,28 +165,19 @@ qx.Class.define("osparc.vipMarket.VipMarket", { }, this); }, - __fetchModels: function(url) { - fetch(url, { - method:"POST" - }) - .then(resp => resp.json()) - .then(anatomicalModelsRaw => { - const allAnatomicalModels = this.self().curateAnatomicalModels(anatomicalModelsRaw); - + __fetchModels: function(vipSubset) { + const licensedItemsStore = osparc.store.LicensedItems.getInstance(); + licensedItemsStore.getVipModels(vipSubset) + .then(allAnatomicalModels => { const store = osparc.store.Store.getInstance(); const contextWallet = store.getContextWallet(); if (!contextWallet) { return; } const walletId = contextWallet.getWalletId(); - const purchasesParams = { - url: { - walletId - } - }; Promise.all([ - osparc.data.Resources.get("licensedItems"), - osparc.data.Resources.fetch("wallets", "purchases", purchasesParams), + licensedItemsStore.getLicensedItems(), + licensedItemsStore.getPurchasedLicensedItems(walletId), ]) .then(values => { const licensedItems = values[0]; @@ -221,17 +185,11 @@ qx.Class.define("osparc.vipMarket.VipMarket", { this.__anatomicalModels = []; allAnatomicalModels.forEach(model => { - const modelId = model["ID"]; + const modelId = model["modelId"]; const licensedItem = licensedItems.find(licItem => licItem["name"] == modelId); if (licensedItem) { - const anatomicalModel = {}; - anatomicalModel["modelId"] = model["ID"]; - anatomicalModel["thumbnail"] = model["Thumbnail"]; - anatomicalModel["name"] = model["Features"]["name"] + " " + model["Features"]["version"]; - anatomicalModel["description"] = model["Description"]; - anatomicalModel["features"] = model["Features"]; - anatomicalModel["date"] = new Date(model["Features"]["date"]); - anatomicalModel["DOI"] = model["DOI"]; + const anatomicalModel = osparc.utils.Utils.deepCloneObject(model); + anatomicalModel["date"] = new Date(anatomicalModel["date"]); // attach license data anatomicalModel["licensedItemId"] = licensedItem["licensedItemId"]; anatomicalModel["pricingPlanId"] = licensedItem["pricingPlanId"]; @@ -269,18 +227,7 @@ qx.Class.define("osparc.vipMarket.VipMarket", { const split = pricingUnit.getName().split(" "); numberOfSeats = parseInt(split[0]); } - const params = { - url: { - licensedItemId - }, - data: { - "wallet_id": walletId, - "pricing_plan_id": pricingPlanId, - "pricing_unit_id": pricingUnitId, - "num_of_seats": numberOfSeats, // this should go away - }, - } - osparc.data.Resources.fetch("licensedItems", "purchase", params) + licensedItemsStore.purchaseLicensedItem(licensedItemId, walletId, pricingPlanId, pricingUnitId, numberOfSeats) .then(() => { const expirationDate = new Date(); expirationDate.setMonth(expirationDate.getMonth() + 1); // rented for one month diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_checkouts_rest.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_checkouts_rest.py index 1a9c7285d0a..0bcdbe9636c 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_checkouts_rest.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_checkouts_rest.py @@ -26,8 +26,9 @@ LicensedItemCheckoutGet, LicensedItemCheckoutGetPage, LicensedItemCheckoutPathParams, + LicensedItemsCheckoutsListQueryParams, ) -from ._models import LicensedItemsPurchasesListQueryParams, LicensedItemsRequestContext +from ._models import LicensedItemsRequestContext _logger = logging.getLogger(__name__) @@ -81,9 +82,9 @@ async def get_licensed_item_checkout(request: web.Request): async def list_licensed_item_checkouts_for_wallet(request: web.Request): req_ctx = LicensedItemsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) - query_params: LicensedItemsPurchasesListQueryParams = ( + query_params: LicensedItemsCheckoutsListQueryParams = ( parse_request_query_parameters_as( - LicensedItemsPurchasesListQueryParams, request + LicensedItemsCheckoutsListQueryParams, request ) )