diff --git a/software/core/oqm-core-api/build.gradle b/software/core/oqm-core-api/build.gradle index 956c9d3800..4f7ee2d459 100644 --- a/software/core/oqm-core-api/build.gradle +++ b/software/core/oqm-core-api/build.gradle @@ -8,7 +8,7 @@ plugins { } group 'com.ebp.openQuarterMaster' -version '5.0.0' +version '6.0.0-SNAPSHOT' repositories { mavenCentral() @@ -39,7 +39,7 @@ dependencies { implementation 'org.jboss.slf4j:slf4j-jboss-logmanager' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' - implementation "com.fasterxml.jackson.datatype:jackson-datatype-javax-money:2.21.3" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-javax-money:2.22.0" implementation 'com.fasterxml.jackson.module:jackson-module-blackbird' // https://mvnrepository.com/artifact/tech.units/indriya @@ -53,8 +53,8 @@ dependencies { implementation 'org.apache.commons:commons-compress:1.28.0' implementation 'org.apache.commons:commons-io:1.3.2' implementation("commons-validator:commons-validator:1.10.1") - implementation 'org.apache.tika:tika-core:3.3.0' - implementation 'uk.org.okapibarcode:okapibarcode:0.5.5' + implementation 'org.apache.tika:tika-core:3.3.1' + implementation 'uk.org.okapibarcode:okapibarcode:0.5.6' implementation("org.javamoney:moneta:1.4.5") diff --git a/software/core/oqm-core-api/docs/metricsAndObservability.md b/software/core/oqm-core-api/docs/metricsAndObservability.md deleted file mode 100644 index b749a8e3ff..0000000000 --- a/software/core/oqm-core-api/docs/metricsAndObservability.md +++ /dev/null @@ -1,3 +0,0 @@ -# Metrics and Observability - -TODO \ No newline at end of file diff --git a/software/core/oqm-core-api/docs/sphinx-test/source/conf.py b/software/core/oqm-core-api/docs/sphinx-test/source/conf.py index 6b4c8b07ed..4e7ab12b8d 100644 --- a/software/core/oqm-core-api/docs/sphinx-test/source/conf.py +++ b/software/core/oqm-core-api/docs/sphinx-test/source/conf.py @@ -36,11 +36,14 @@ 'github_repo': 'OpenQuarterMaster', 'github_banner': True, "github_button": False, - "show_relbars": True + "show_relbars": True, # 'github_count': True, # 'extra_nav_links': { # 'Index': 'genindex.html', # } + + 'body_max_width': 'none', + 'page_width': '90%', } html_sidebars = { '**': [ diff --git a/software/core/oqm-core-api/docs/sphinx-test/source/design_decisions.rst b/software/core/oqm-core-api/docs/sphinx-test/source/design_decisions.rst index e73715a851..54a850fb7a 100644 --- a/software/core/oqm-core-api/docs/sphinx-test/source/design_decisions.rst +++ b/software/core/oqm-core-api/docs/sphinx-test/source/design_decisions.rst @@ -10,30 +10,60 @@ Operational / Architecture Decisions This section talks to how we arrived at the decisions surrounding how we have built the service, and why we chose the tools we did. -Quarkus -------- +Quarkus / Java +-------------- + +`Quarkus `_ is a fully featured, lean, and performant framework for building many things, but most notably web, container-native services. + +Quarkus is Java-based, and thus is the language we are working in. Java was chosen as is a good choice for such services, being a strongly typed language with a mature +framework ecosystem. + +Other options could include Spring, or things in other Container Deployment -------------------- +`Containers `_ provide an extreme level of flexibility and ease of deployment in a variety of environments. + +If it can run Podman/ Docker, it can run the core API. + MongoDB ------- +`MongoDB `_ Is a highly flexible and scalable database. We chose this over \*.sql to leverage: + + * flexibility in data model; can directly use polymorphic objects within the same collection + * scalability; naturally a clustered system, can grow with OQM as a cluster + JWT Auth -------- +`JWTs `_ are a flexible, implementation independent method for authorization. Being a widely +used web standard, was a natural choice + Kafka Messaging --------------- +`Apache Kafka `_ is a widely used and supported messaging platform. What made it stand out was a relative level of simplicity, +both in usage and in deployment. + Optionality ^^^^^^^^^^^ +It is important to the project to be flexible and modular, and Kafka frankly eats up a lot of computing resources. By making the Core API only optionally +include connection to a Kafka server, we can omit that overhead when we don't need to include it when the downstream functionality is unused. + OpenTelemetry ------------- +`OpenTelemetry `_ is a widely used standard for transferring metrics, logs, and tracing information to tools that can use that information. +It made sense to adopt, as is largely vendor agnostic. + Optionality ^^^^^^^^^^^ +As with Kafka, if users don't want the overhead of a metrics stack, the should not need to provide and use one. + Functional / Design Decisions (major) ===================================== diff --git a/software/core/oqm-core-api/docs/sphinx-test/source/running/03_configuration.rst b/software/core/oqm-core-api/docs/sphinx-test/source/running/03_configuration.rst new file mode 100644 index 0000000000..a99b78dbd2 --- /dev/null +++ b/software/core/oqm-core-api/docs/sphinx-test/source/running/03_configuration.rst @@ -0,0 +1,235 @@ +Configuration Reference +####################### + +Below is the configuration reference to configure your Core API instance. + +All values are optional, unless otherwise marked by: *Required* + +Core API Specific Configuration +=============================== + +.. list-table:: + :header-rows: 1 + + * - Config Key + - Description + - Values (Examples) + - Default + * - .. code-block:: none + + service.ops.currency + - Sets the currency to be used by the system. Use any value that can be passed to + `Currency.getInstance() `_ + - .. code-block:: none + + USD + .. code-block:: none + + GBP + - .. code-block:: none + + USD + * - .. code-block:: none + + service.item.expiryCheck.cron + - Sets the interval at which expiry checks happen. In a cron format. `Docs `_ + - .. code-block:: none + + 0 0 * ? * * # Every Hour + - .. code-block:: none + + 0 0 * ? * * # Every Hour + * - .. code-block:: none + + service.tempDir + - The directory to be used for temporary file creation and storage. Used for + - .. code-block:: none + + /tmp + - .. code-block:: none + + /tmp/oqm-core-api + * - .. code-block:: none + + service.image.resizing.enabled + - If the service is to resize images when they are uploaded (images for attached images, not images uploaded as generic files) + - .. code-block:: none + + true + .. code-block:: none + + false + - .. code-block:: none + + true + * - .. code-block:: none + + service.image.resizing.height + - The height to resize images to, in number of pixels. + - .. code-block:: none + + 500 + - .. code-block:: none + + 750 + * - .. code-block:: none + + service.image.resizing.width + - The width to resize images to, in number of pixels. + - .. code-block:: none + + 500 + - .. code-block:: none + + ${service.image.resizing.height} + * - .. code-block:: none + + service.image.resizing.savedType + - The image format to use to save the resulting images as. + - Either: + + .. code-block:: none + + jpg + + or: + + .. code-block:: none + + png + - .. code-block:: none + + jpg + +MongoDB Configuration +===================== + +For Mongo configuration, we leverage the settings as presented by Quarkus. For convenience, we have the most important configuration below, but +further options can be found on the `Quarkus MongoDB Configuration `_ documentation. + +Please see :doc:`04_mongo` for more information on how MongoDB is leveraged and used by the Core API. + +.. list-table:: + :header-rows: 1 + + * - Config Key + - Description + - Values (Examples) + - Default + * - .. code-block:: none + + QUARKUS_MONGODB_CONNECTION-STRING + + *Required* + - The connection string to actually connect to Mongo + - .. code-block:: none + + mongodb://${user}:${pass}@${host}:${port} + - None, must supply this value. + * - .. code-block:: none + + QUARKUS_MONGODB_DATABASE + - The database to use for OQM. Also the prefix to use for specific datasets. + - .. code-block:: none + + openQuarterMaster + - .. code-block:: none + + openQuarterMaster + +Kafka Configuration +===================== + +For Kafka configuration, we leverage the settings as presented by Quarkus. For convenience, we have the most important configuration below, but +further options can be found on the `Quarkus Kafka Configuration `_ documentation. + +.. list-table:: + :header-rows: 1 + + * - Config Key + - Description + - Values (Examples) + - Default + * - .. code-block:: none + + mp.messaging.outgoing.events-outgoing.enabled + + *Required* + - Whether or not to enable the messaging functionality. + - Either: + + .. code-block:: none + + true + + or: + + .. code-block:: none + + false + - None, must supply this value. + * - .. code-block:: none + + mp.messaging.outgoing.events-outgoing.bootstrap.servers + + *Required*, if kafka enabled + - The kafka bootstrap server(s) to connect to. + - .. code-block:: none + + OUTSIDE://{infra.kafka.host}:{infra.kafka.port} + + - None, must supply this value (if kafka enabled). + +JWT Configuration +===================== + +For JWT configuration, we leverage the settings as presented by Quarkus. For convenience, we have the most important configuration below, but +further options can be found on the `Quarkus JWT Configuration `_ documentation. + +.. list-table:: + :header-rows: 1 + + * - Config Key + - Description + - Values (Examples) + - Default + * - .. code-block:: none + + smallrye.jwt.verify.key.location + + *Required* + + Or, some configuration that provides the certs for JWT verification. + - The location of where to retrieve certs to verify JWT's. Config property allows for a specified external or internal + location of the public key. The value can be a relative path or a URL. + - .. code-block:: none + + http://oqm-infra-keycloak:8080/realms/oqm/protocol/openid-connect/certs + + .. code-block:: none + + /path/to/cert.pub + - None, must supply this value. + +Metrics/OpenTelemetry Configuration +===================== + +For OpenTelemetry configuration, we leverage the settings as presented by Quarkus. For convenience, we have the most important configuration below, but +further options can be found on the `Quarkus OpenTelemetry Configuration `_ documentation. + +.. list-table:: + :header-rows: 1 + + * - Config Key + - Description + - Values (Examples) + - Default + * - .. code-block:: none + + quarkus.otel.exporter.otlp.endpoint + + - Where to export OpenTelemetry metrics to. + - .. code-block:: none + + http://#{host}:4317 + - None. diff --git a/software/core/oqm-core-api/docs/sphinx-test/source/running/04_mongo.rst b/software/core/oqm-core-api/docs/sphinx-test/source/running/04_mongo.rst new file mode 100644 index 0000000000..9b1eac6615 --- /dev/null +++ b/software/core/oqm-core-api/docs/sphinx-test/source/running/04_mongo.rst @@ -0,0 +1,11 @@ +MongoDB Reference +####################### + +This guide is an explainer to how MongoDB is leveraged by the Core API + +Databases +========= + +TODO + + diff --git a/software/core/oqm-core-api/docs/sphinx-test/source/running/05_jwt.rst b/software/core/oqm-core-api/docs/sphinx-test/source/running/05_jwt.rst new file mode 100644 index 0000000000..6830cd8769 --- /dev/null +++ b/software/core/oqm-core-api/docs/sphinx-test/source/running/05_jwt.rst @@ -0,0 +1,8 @@ +JWT Reference +####################### + +This guide is an explainer to how JWT's are used by the Core API, and what needs to be included. + + + + diff --git a/software/core/oqm-core-api/docs/sphinx-test/source/running/index.rst b/software/core/oqm-core-api/docs/sphinx-test/source/running/index.rst index 7edec7fec6..b5d2b84446 100644 --- a/software/core/oqm-core-api/docs/sphinx-test/source/running/index.rst +++ b/software/core/oqm-core-api/docs/sphinx-test/source/running/index.rst @@ -10,3 +10,6 @@ This section is concerned with how to run, configure, and otherwise manage your 01_prerequisites 02_basic_quickstart + 03_configuration + 04_mongo + 05_jwt diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/SchemaUpgradeHealthCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/SchemaUpgradeHealthCheck.java deleted file mode 100644 index c83545a74b..0000000000 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/SchemaUpgradeHealthCheck.java +++ /dev/null @@ -1,28 +0,0 @@ -package tech.ebp.oqm.core.api.health; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.health.*; -import tech.ebp.oqm.core.api.service.schemaVersioning.ObjectSchemaUpgradeService; - -@Startup -@ApplicationScoped -public class SchemaUpgradeHealthCheck implements HealthCheck { - - @Inject - ObjectSchemaUpgradeService objectSchemaUpgradeService; - - @Override - public HealthCheckResponse call() { - HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.builder() - .name("Database Schema Upgrade"); - - if(objectSchemaUpgradeService.upgradeRan()){ - responseBuilder = responseBuilder.up(); - } else { - responseBuilder = responseBuilder.down(); - } - - return responseBuilder.build(); - } -} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/LivenessHealthCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/LivenessHealthCheck.java new file mode 100644 index 0000000000..790c7faa08 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/LivenessHealthCheck.java @@ -0,0 +1,28 @@ +package tech.ebp.oqm.core.api.health.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.Liveness; +import tech.ebp.oqm.core.api.health.utils.GenericHealthCheck; +import tech.ebp.oqm.core.api.health.utils.HasLivenessCheck; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; + +@Liveness +@ApplicationScoped +public class LivenessHealthCheck extends GenericHealthCheck { + + @Inject + LivenessHealthCheck(Instance providers) { + super("Service Health - Liveness", providers); + } + + public LivenessHealthCheck() { + super("Service Health - Liveness", null); + } + + @Override + protected HealthStatus getStatus(HasLivenessCheck provider) { + return provider.getLivenessStatus(); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/ReadinessHealthCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/ReadinessHealthCheck.java new file mode 100644 index 0000000000..4b8715dca2 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/ReadinessHealthCheck.java @@ -0,0 +1,28 @@ +package tech.ebp.oqm.core.api.health.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.Readiness; +import tech.ebp.oqm.core.api.health.utils.GenericHealthCheck; +import tech.ebp.oqm.core.api.health.utils.HasReadinessCheck; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; + +@Readiness +@ApplicationScoped +public class ReadinessHealthCheck extends GenericHealthCheck { + + @Inject + ReadinessHealthCheck(Instance providers) { + super("Service Health - Readiness", providers); + } + + public ReadinessHealthCheck() { + super("Service Health - Readiness", null); + } + + @Override + protected HealthStatus getStatus(HasReadinessCheck provider) { + return provider.getReadinessStatus(); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/StartupHealthCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/StartupHealthCheck.java new file mode 100644 index 0000000000..e1eb632d73 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/service/StartupHealthCheck.java @@ -0,0 +1,28 @@ +package tech.ebp.oqm.core.api.health.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.Startup; +import tech.ebp.oqm.core.api.health.utils.GenericHealthCheck; +import tech.ebp.oqm.core.api.health.utils.HasStartupCheck; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; + +@Startup +@ApplicationScoped +public class StartupHealthCheck extends GenericHealthCheck { + + @Inject + StartupHealthCheck(Instance providers) { + super("Service Health - Startup", providers); + } + + public StartupHealthCheck() { + super("Service Health - Startup", null); + } + + @Override + protected HealthStatus getStatus(HasStartupCheck provider) { + return provider.getStartupStatus(); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/GenericHealthCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/GenericHealthCheck.java new file mode 100644 index 0000000000..e884222960 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/GenericHealthCheck.java @@ -0,0 +1,34 @@ +package tech.ebp.oqm.core.api.health.utils; + +import jakarta.enterprise.inject.Instance; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; + +public abstract class GenericHealthCheck implements HealthCheck { + private final String healthCheckName; + private final Instance providers; + + public GenericHealthCheck(String healthCheckName, Instance providers) { + this.healthCheckName = healthCheckName; + this.providers = providers; + } + + protected abstract HealthStatus getStatus(T provider); + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named(this.healthCheckName); + boolean allUp = true; + + for (T provider : this.providers) { + HealthStatus status = this.getStatus(provider); + boolean up = status.isUp(); + allUp &= up; + builder.withData(status.getName() + ".up", up); + builder.withData(status.getName() + ".status", status.getStatusMessage()); + } + + return (allUp ? builder.up() : builder.down()).build(); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasHealthCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasHealthCheck.java new file mode 100644 index 0000000000..043b75ad9b --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasHealthCheck.java @@ -0,0 +1,3 @@ +package tech.ebp.oqm.core.api.health.utils; + +public interface HasHealthCheck {} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasLivenessCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasLivenessCheck.java new file mode 100644 index 0000000000..a202084fdb --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasLivenessCheck.java @@ -0,0 +1,5 @@ +package tech.ebp.oqm.core.api.health.utils; + +public interface HasLivenessCheck extends HasHealthCheck { + HealthStatus getLivenessStatus(); +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasReadinessCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasReadinessCheck.java new file mode 100644 index 0000000000..a7bc7001d2 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasReadinessCheck.java @@ -0,0 +1,5 @@ +package tech.ebp.oqm.core.api.health.utils; + +public interface HasReadinessCheck extends HasHealthCheck { + HealthStatus getReadinessStatus(); +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasStartupCheck.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasStartupCheck.java new file mode 100644 index 0000000000..27d07c44df --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HasStartupCheck.java @@ -0,0 +1,5 @@ +package tech.ebp.oqm.core.api.health.utils; + +public interface HasStartupCheck extends HasHealthCheck { + HealthStatus getStartupStatus(); +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HealthStatus.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HealthStatus.java new file mode 100644 index 0000000000..16f016b763 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/health/utils/HealthStatus.java @@ -0,0 +1,24 @@ +package tech.ebp.oqm.core.api.health.utils; + +import lombok.Getter; + +@Getter +public class HealthStatus { + private final String name; + private volatile boolean up = false; + private volatile String statusMessage = "Status not set"; + + public HealthStatus(String name) { + this.name = name; + } + + public void markUp(String message) { + up = true; + statusMessage = message; + } + + public void markDown(String message) { + up = false; + statusMessage = message; + } +} \ No newline at end of file diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InBlockEndpoints.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InBlockEndpoints.java index e2227e87ea..820b8cb524 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InBlockEndpoints.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InBlockEndpoints.java @@ -42,11 +42,11 @@ public class InBlockEndpoints extends MainObjectProvider { @Getter @Inject StoredService objectService; - + @Getter @Inject InventoryItemService inventoryItemService; - + @Getter @Inject StorageBlockService storageBlockService; @@ -57,43 +57,43 @@ public class InBlockEndpoints extends MainObjectProvider { @Getter @PathParam("itemId") ObjectId itemId; - + @Getter @PathParam("blockId") ObjectId blockId; private InventoryItem inventoryItem; private StorageBlock storageBlock; - + private StorageBlock getStorageBlock() { if (this.storageBlock == null) { this.storageBlock = this.storageBlockService.get(this.getOqmDbIdOrName(), this.blockId); - + } return this.storageBlock; } - + private InventoryItem getInventoryItem() { if (this.inventoryItem == null) { this.inventoryItem = this.inventoryItemService.get(this.getOqmDbIdOrName(), this.itemId); - - if(!this.inventoryItem.getStorageBlocks().contains(this.getStorageBlock().getId())){ + + if(!this.inventoryItem.usesStorageBlock(this.getStorageBlock().getId())){ throw new NotFoundException("Storage block given not found in the given item"); } } return this.inventoryItem; } - + private Stored applyDefaults(Stored stored){ stored.applyDefaultsFromItem(this.getInventoryItem()); return stored; } - + private SearchResult applyDefaults(SearchResult searchResult){ searchResult.getResults().forEach(this::applyDefaults); return searchResult; } - + @GET @Path("stored") @Operation( diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/messaging/EventNotificationWrapper.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/messaging/EventNotificationWrapper.java index f68bda7b1f..766da8d280 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/messaging/EventNotificationWrapper.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/messaging/EventNotificationWrapper.java @@ -1,7 +1,9 @@ package tech.ebp.oqm.core.api.model.messaging; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; import org.bson.types.ObjectId; +import tech.ebp.oqm.core.api.model.object.history.EventType; import tech.ebp.oqm.core.api.model.object.history.ObjectHistoryEvent; @Data @@ -12,4 +14,14 @@ public class EventNotificationWrapper { private ObjectId database; private String objectName; private ObjectHistoryEvent event; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public EventType getEventType() { + return this.getEvent().getType(); + } + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public ObjectId getObjectId() { + return this.getEvent().getObjectId(); + } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/InventoryItem.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/InventoryItem.java index 01fc0266b7..013f1195c7 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/InventoryItem.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/InventoryItem.java @@ -1,5 +1,6 @@ package tech.ebp.oqm.core.api.model.object.storage.items; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; @@ -9,6 +10,7 @@ import lombok.NonNull; import lombok.ToString; import lombok.experimental.SuperBuilder; +import org.bson.codecs.pojo.annotations.BsonIgnore; import org.bson.types.ObjectId; import org.eclipse.microprofile.openapi.annotations.media.Schema; import tech.ebp.oqm.core.api.model.object.FileAttachmentContaining; @@ -19,6 +21,7 @@ import tech.ebp.oqm.core.api.model.object.storage.items.pricing.StoredPricing; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.stats.ItemStoredStats; +import tech.ebp.oqm.core.api.model.object.storage.storageBlock.StorageBlock; import tech.ebp.oqm.core.api.model.units.OqmProvidedUnits; import tech.ebp.oqm.core.api.model.validation.annotations.UniqueLabeledCollection; import tech.ebp.oqm.core.api.model.validation.annotations.ValidItemUnit; @@ -33,6 +36,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.stream.Stream; /** * Describes a type of inventory item. @@ -46,9 +50,9 @@ @ValidItemUnit @Schema(description = "A type of item that is stored. Does not describe the exact items stored, but their type. Exact items stored are described by storeds.") public class InventoryItem extends ImagedMainObject implements FileAttachmentContaining { - - public static final int CUR_SCHEMA_VERSION = 4; - + + public static final int CUR_SCHEMA_VERSION = 5; + /** * The name of this inventory item */ @@ -57,7 +61,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @NotBlank(message = "Name cannot be blank") @Schema(required = true, description = "The name of the item.", examples = {"Soap"}) private String name; - + /** * The type of storage this item uses. */ @@ -65,7 +69,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @NotNull @Schema(required = true, description = "The type of storage this item uses.") private StorageType storageType; - + /** * Description of the item */ @@ -74,7 +78,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @Schema(required = false, description = "The description of this item.", examples = {""}) private String description = ""; - + /** * The general identifiers for this item */ @@ -83,7 +87,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @UniqueLabeledCollection private LinkedHashSet<@NotNull Identifier> identifiers = new LinkedHashSet<>(); - + /** * ID generators for this particular item's stored. */ @@ -91,7 +95,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @NotNull @lombok.Builder.Default private LinkedHashSet<@NotNull ObjectId> idGenerators = new LinkedHashSet<>(); - + /** * Categories this item belongs to. */ @@ -100,7 +104,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @Schema(required = false, description = "The categories this item belongs to.", examples = {"[]"}) private List<@NotNull ObjectId> categories = new ArrayList<>(); - + /** * The map of where the items are stored. *

@@ -111,9 +115,9 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @NonNull @NotNull @lombok.Builder.Default - @Schema(required = false, description = "The storage blocks this item is stored in.", examples = {"[]"}) - private LinkedHashSet<@NotNull ObjectId> storageBlocks = new LinkedHashSet<>(); - + @Schema(required = false, description = "The storage blocks this item is stored in.", examples = {"[]"}) + private List<@NotNull StorageBlockSettings> storageBlocks = new ArrayList<>(); + /** * Files that have been attached to the item. */ @@ -122,7 +126,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @Schema(required = false, description = "Files to attach to the item.", examples = {"[]"}) private Set<@NotNull ObjectId> attachedFiles = new LinkedHashSet<>(); - + /** * Default pricing for items stored. *

@@ -133,7 +137,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @UniqueLabeledCollection private LinkedHashSet<@NotNull StoredPricing> defaultPrices = new LinkedHashSet<>(); - + /** * Links to external resources related to the item */ @@ -142,7 +146,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @UniqueLabeledCollection private LinkedHashSet<@NotNull AssociatedLink> associatedLinks = new LinkedHashSet<>(); - + /** * The default format that should be applied to new stored items if not otherwise specified. * @@ -151,13 +155,13 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @ValidStoredLabelFormat private String defaultLabelFormat = null; - + @NonNull @NotNull @lombok.Builder.Default @Schema(required = false, description = "The state of notifications sent about item stored state (low stock, expiry)") private ItemNotificationStatus notificationStatus = new ItemNotificationStatus(); - + /** * When before a stored item expired to send a warning out about that expiration. *

@@ -168,7 +172,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @Schema(required = false, description = "When before a stored item becomes expired to alert a warning about said expiration. Defaults to Zero.") private Duration expiryWarningThreshold = Duration.ZERO; - + /** * The threshold of low stock for the entire object. *

@@ -190,7 +194,7 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @Schema(required = true, description = "The unit to use to track these items. Check the compatible units endpoints to see what is available.") public Unit unit = OqmProvidedUnits.UNIT; - + /** * The stats for the stored items. *

@@ -201,7 +205,18 @@ public class InventoryItem extends ImagedMainObject implements FileAttachmentCon @lombok.Builder.Default @Schema(required = false, readOnly = true, description = "Stats about this item's stored instances.") private ItemStoredStats stats = null; - + + @BsonIgnore + @JsonIgnore + public Stream getStorageBlockIds() { + return this.storageBlocks.stream().map(StorageBlockSettings::getStorageBlock); + } + + @BsonIgnore + @JsonIgnore + public boolean usesStorageBlock(ObjectId storageBlockId) { + return this.getStorageBlockIds().anyMatch(curId -> curId.equals(storageBlockId)); + } @Schema(defaultValue = InventoryItem.CUR_SCHEMA_VERSION+"") @Override diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/StorageBlockSettings.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/StorageBlockSettings.java new file mode 100644 index 0000000000..3fb8c96f34 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/StorageBlockSettings.java @@ -0,0 +1,57 @@ +package tech.ebp.oqm.core.api.model.object.storage.items; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import org.bson.codecs.pojo.annotations.BsonIgnore; +import org.bson.types.ObjectId; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import tech.ebp.oqm.core.api.model.object.storage.items.notification.StorageBlockNotificationStatus; + +import javax.measure.Quantity; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class StorageBlockSettings { + + @NonNull + @NotNull + private ObjectId storageBlock; + + /** + * Notes about how the items are stored in the storage block, if relevant + */ + private String notes; + + /** + * The threshold of low stock for the entire object. + *

+ * Null for no threshold, Quantity with compatible unit to set the threshold. + */ + @lombok.Builder.Default + @Schema(required = false, description = "The threshold of low stock for the associated storage block. Null for no threshold. Unit must be compatible with item's.") + private Quantity lowStockThreshold = null; + + @BsonIgnore + @JsonIgnore + public boolean hasLowStockThreshold() { + return this.lowStockThreshold != null; + } + + @NonNull + @NotNull + @Builder.Default + private StorageBlockNotificationStatus notificationStatus = new StorageBlockNotificationStatus(); + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public boolean isHasSettings() { + return this.hasLowStockThreshold() || (this.notes != null && !this.notes.isBlank()); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/notification/StorageBlockNotificationStatus.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/notification/StorageBlockNotificationStatus.java new file mode 100644 index 0000000000..a62a24c287 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/notification/StorageBlockNotificationStatus.java @@ -0,0 +1,16 @@ +package tech.ebp.oqm.core.api.model.object.storage.items.notification; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class StorageBlockNotificationStatus { + + @Builder.Default + private boolean lowStock = false; +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/BasicStatsContaining.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/BasicStatsContaining.java index 5c5758b798..de36678a95 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/BasicStatsContaining.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/BasicStatsContaining.java @@ -1,5 +1,6 @@ package tech.ebp.oqm.core.api.model.object.storage.items.stored.stats; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; @@ -19,19 +20,27 @@ @NoArgsConstructor @SuperBuilder public class BasicStatsContaining { - + @lombok.Builder.Default private long numStored = 0; - + + /** + * TODO:: rename to numStoredLowStock + */ @lombok.Builder.Default private long numLowStock = 0; - + @lombok.Builder.Default private long numExpiryWarn = 0; - + @lombok.Builder.Default private long numExpired = 0; - + @lombok.Builder.Default private LinkedHashSet<@NotNull TotalPricing> prices = new LinkedHashSet<>(); + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public boolean isHasLowStockStored() { + return this.numLowStock > 0; + } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/ItemStoredStats.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/ItemStoredStats.java index 24fe19f443..13337ebb67 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/ItemStoredStats.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/ItemStoredStats.java @@ -29,11 +29,11 @@ @ToString(callSuper = true) @SuperBuilder public class ItemStoredStats extends StatsWithTotalContaining { - + public ItemStoredStats(Unit unit, Set defaultPrices) { super(unit); this.storageBlockStats = new LinkedHashMap<>(); - + this.setPrices( defaultPrices.stream().map( p -> TotalPricing.builder() @@ -46,17 +46,22 @@ public ItemStoredStats(Unit unit, Set defaultPrices) { ).collect(Collectors.toCollection(LinkedHashSet::new)) ); } - + @NonNull @NotNull @lombok.Builder.Default private Map storageBlockStats = new LinkedHashMap<>(); - + @lombok.Builder.Default private boolean lowStock = false; - + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public boolean isHasLowStockInBlock() { + return this.storageBlockStats.values().stream().anyMatch(StoredInBlockStats::isLowStock); + } + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public boolean isAnyLowStock() { - return this.lowStock || this.getNumLowStock() != 0; + return this.lowStock || this.isHasLowStockStored() || this.isHasLowStockInBlock(); } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/StoredInBlockStats.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/StoredInBlockStats.java index 701ce213b7..41e5baaded 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/StoredInBlockStats.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/storage/items/stored/stats/StoredInBlockStats.java @@ -1,5 +1,6 @@ package tech.ebp.oqm.core.api.model.object.storage.items.stored.stats; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -19,7 +20,15 @@ public class StoredInBlockStats extends StatsWithTotalContaining { public StoredInBlockStats(Unit unit){ super(unit); } - + + @lombok.Builder.Default + private boolean lowStock = false; + @lombok.Builder.Default private boolean hasStored = false; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public boolean isAnyLowStock() { + return this.lowStock || this.isHasLowStockStored(); + } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InventoryItemSearch.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InventoryItemSearch.java index 8391fe316b..e1c22810d2 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InventoryItemSearch.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InventoryItemSearch.java @@ -24,7 +24,7 @@ public class InventoryItemSearch extends SearchKeyAttObject { @QueryParam("name") @Parameter(description = "The name of the item to search for.") String name; - + @QueryParam("storageTypes") List storageTypes; @QueryParam("itemCategories") List categories; @QueryParam("inStorageBlock") List inStorageBlocks; @@ -36,13 +36,13 @@ public class InventoryItemSearch extends SearchKeyAttObject { @QueryParam("hasLowStock") Boolean hasLowStock; @QueryParam("hasNoLowStock") Boolean hasNoLowStock; @QueryParam("identifier") List identifiers; - + //TODO:: object specific fields, add to bson filter list - + @Override public List getSearchFilters() { List filters = super.getSearchFilters(); - + if (hasValue(this.getName())) { filters.add( SearchUtils.getBasicSearchFilter("name", this.getName()) @@ -71,7 +71,7 @@ public List getSearchFilters() { if(hasValue(this.getInStorageBlocks())){ filters.add(or( this.getInStorageBlocks().stream().map((ObjectId storageBlockId) -> { - return in("storageBlocks", storageBlockId); + return eq("storageBlocks.storageBlock", storageBlockId); }).toList() )); } @@ -120,7 +120,7 @@ public List getSearchFilters() { if(hasValue(this.getHasNoLowStock())){ if(this.getHasNoLowStock()){ filters.add( - eq("stats.anyLowStock", true) + eq("stats.anyLowStock", false) ); } } @@ -133,7 +133,7 @@ public List getSearchFilters() { } filters.add(Filters.or(typeFilterList)); } - + return filters; } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueLabeledCollection.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueLabeledCollection.java index 92d4462ff5..604f61632c 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueLabeledCollection.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueLabeledCollection.java @@ -24,10 +24,10 @@ @Constraint(validatedBy = UniqueLabeledCollectionValidator.class) @Documented public @interface UniqueLabeledCollection { - - String message() default "Unit was not one of allowed units."; - + + String message() default "Multiple labeled items were found with the same label."; + Class[] groups() default {}; - + Class[] payload() default {}; } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueStorageBlockSettingsCollection.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueStorageBlockSettingsCollection.java new file mode 100644 index 0000000000..84f86366eb --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/annotations/UniqueStorageBlockSettingsCollection.java @@ -0,0 +1,31 @@ +package tech.ebp.oqm.core.api.model.validation.annotations; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import tech.ebp.oqm.core.api.model.validation.validators.UniqueLabeledCollectionValidator; +import tech.ebp.oqm.core.api.model.validation.validators.UniqueStorageBlockSettingsCollectionValidator; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Custom annotation to check that storage block settings are unique to what storage blocks they associate. + */ +@Target({FIELD, METHOD, PARAMETER}) +@Retention(RUNTIME) +@Constraint(validatedBy = UniqueStorageBlockSettingsCollectionValidator.class) +@Documented +public @interface UniqueStorageBlockSettingsCollection { + + String message() default "Duplicative storage block settings were found."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/ItemUnitValidator.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/ItemUnitValidator.java index ac4cddcff6..b80618ae95 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/ItemUnitValidator.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/ItemUnitValidator.java @@ -2,6 +2,7 @@ import jakarta.validation.ConstraintValidatorContext; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.units.OqmProvidedUnits; import tech.ebp.oqm.core.api.model.units.UnitUtils; import tech.ebp.oqm.core.api.model.validation.annotations.ValidItemUnit; @@ -12,11 +13,11 @@ import java.util.List; public class ItemUnitValidator extends Validator { - + @Override public boolean isValid(InventoryItem item, ConstraintValidatorContext constraintValidatorContext) { List errs = new ArrayList<>(); - + Unit unit = item.getUnit(); switch (item.getStorageType().storedType){ case AMOUNT -> { @@ -31,7 +32,16 @@ public boolean isValid(InventoryItem item, ConstraintValidatorContext constraint } } } - + + item.getStorageBlocks() + .stream() + .filter(StorageBlockSettings::hasLowStockThreshold) + .forEach(block -> { + if(!UnitUtils.UNIT_LIST.contains(unit)){ + errs.add("Invalid unit for storage block "+block.getStorageBlock()+": " + unit.toString() + " not applicable for item storage. Must use unit in the unit list."); + } + }); + return this.processValidationResults(errs, constraintValidatorContext); } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/UniqueStorageBlockSettingsCollectionValidator.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/UniqueStorageBlockSettingsCollectionValidator.java new file mode 100644 index 0000000000..cb87597b25 --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/UniqueStorageBlockSettingsCollectionValidator.java @@ -0,0 +1,45 @@ +package tech.ebp.oqm.core.api.model.validation.validators; + +import jakarta.validation.ConstraintValidatorContext; +import org.bson.types.ObjectId; +import tech.ebp.oqm.core.api.model.object.Labeled; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; +import tech.ebp.oqm.core.api.model.validation.annotations.UniqueLabeledCollection; +import tech.ebp.oqm.core.api.model.validation.annotations.UniqueStorageBlockSettingsCollection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class UniqueStorageBlockSettingsCollectionValidator extends Validator> { + + @Override + public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) { + List errs = new ArrayList<>(); + + if (collection == null) { + return true; + } else { + if(collection.stream().anyMatch(sbs -> sbs.getStorageBlock() == null)){ + errs.add("Storage block settings cannot have a null storage block"); + } else { + + Set elements = new HashSet<>(); + + List duplicateStorageBlocks = collection.stream() + .map(StorageBlockSettings::getStorageBlock) + .filter(n->!elements.add(n)) + .map(ObjectId::toString) + .toList(); + + if (!duplicateStorageBlocks.isEmpty()) { + errs.add("Multiple storage block settings found with the same storage block: " + String.join(", ", duplicateStorageBlocks)); + } + } + } + + return this.processValidationResults(errs, constraintValidatorContext); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/LifecycleBean.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/LifecycleBean.java index 5dcbb2e772..93fcf8818a 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/LifecycleBean.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/LifecycleBean.java @@ -8,10 +8,8 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; -import tech.ebp.oqm.core.api.model.object.upgrade.TotalUpgradeResult; import tech.ebp.oqm.core.api.service.TempFileService; import tech.ebp.oqm.core.api.service.mongo.CustomUnitService; -import tech.ebp.oqm.core.api.service.schemaVersioning.ObjectSchemaUpgradeService; import tech.ebp.oqm.core.api.service.serviceState.db.OqmDatabaseService; import java.nio.file.Paths; @@ -24,31 +22,28 @@ @Singleton @Slf4j public class LifecycleBean { - + @ConfigProperty(name="service.version") String serviceVersion; - + @ConfigProperty(name="service.apiVersion") String apiVersion; - + @Inject CustomUnitService customUnitService; - + @Inject TempFileService tempFileService; - - @Inject - OqmDatabaseService dbService; @Inject - ObjectSchemaUpgradeService objectSchemaUpgradeService; + OqmDatabaseService dbService; private ZonedDateTime startDateTime; - + public static void logConfig(){ if (log.isDebugEnabled()) { TreeMap configMap = new TreeMap<>(); - + for(String curProp : ConfigProvider.getConfig().getPropertyNames()){ String value; try { @@ -58,10 +53,10 @@ public static void logConfig(){ } configMap.put(curProp, value); } - + StringBuilder sb = new StringBuilder(); for (String curProp : configMap.keySet()) { - + sb.append('\t'); sb.append(curProp); sb.append('='); @@ -71,11 +66,11 @@ public static void logConfig(){ log.debug("Configuration: \n{}", sb); } } - + private void startLogAnnounce(){ this.startDateTime = ZonedDateTime.now(); log.info("Open QuarterMaster Core API Server starting."); - + if(log.isInfoEnabled()) { // Image: https://www.text-image.com/convert/ascii.html // Text: https://manytools.org/hacker-tools/ascii-banner/ (Colossal font) @@ -119,35 +114,19 @@ private void startLogAnnounce(){ this.apiVersion ); } - + logConfig(); log.info("Starting in directory: {}", Paths.get("").toAbsolutePath()); } - + void onStart( @Observes StartupEvent ev ) { this.startLogAnnounce(); - //ensures the db service bean is initialized, and the extension has had time to init - this.dbService.collectionStats(); - //ensures the unit service bean is initialized, and the extension had existing custom units read in - this.customUnitService.collectionStats(); - //ensures we can write to temp dir - this.tempFileService.getTempDir("test", "dir"); - // Upgrade the db schema - //TODO:: create flag service to check if things initted right. Setup filter to check this flag to reject requests until setup done. - Optional schemaUpgradeResult = this.objectSchemaUpgradeService.updateSchema(); - if(schemaUpgradeResult.isEmpty()){ - log.warn("Did not upgrade schema at start."); - } else { - log.info("Schema upgrade result: {}", schemaUpgradeResult.get()); - //TODO:: rescan inv update stats - } - log.info("Done with initial startup tasks."); } - + void onStop( @Observes ShutdownEvent ev diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/MongoDbInit.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/MongoDbInit.java index 3579e27e63..0d262df381 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/MongoDbInit.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/MongoDbInit.java @@ -6,17 +6,23 @@ import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import tech.ebp.oqm.core.api.model.object.upgrade.TotalUpgradeResult; import tech.ebp.oqm.core.api.service.mongo.InventoryItemService; -import tech.ebp.oqm.core.api.service.mongo.MongoDbAwareService; import tech.ebp.oqm.core.api.service.mongo.MongoService; +import tech.ebp.oqm.core.api.service.schemaVersioning.ObjectSchemaUpgradeService; import tech.ebp.oqm.core.api.service.serviceState.InstanceMutexService; import tech.ebp.oqm.core.api.service.serviceState.db.DbCacheEntry; import tech.ebp.oqm.core.api.service.serviceState.db.OqmDatabaseService; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; +import tech.ebp.oqm.core.api.health.utils.HasReadinessCheck; + +import java.util.Optional; @Singleton @Slf4j -public class MongoDbInit { +public class MongoDbInit implements HasReadinessCheck { @Inject InventoryItemService inventoryItemService; @@ -31,6 +37,9 @@ public class MongoDbInit { @Any Instance> mongoServices; + @Inject + ObjectSchemaUpgradeService objectSchemaUpgradeService; + /** * This was introduced in version 4.4.8 ~ May 15, 2026 *

@@ -39,33 +48,64 @@ public class MongoDbInit { * This can be removed once all inventory items have mutexes. */ private void ensureItemMutexesExist() { - log.info("Ensuring inventory item mutexes exist."); - - for (DbCacheEntry curDb : this.oqmDatabaseService.getDatabases()) { - log.info("Ensuring inventory item mutexes exist for database: {}", curDb.getDbName()); - this.inventoryItemService.iterator(curDb.getDbId().toHexString()).forEachRemaining((item) -> { - this.instanceMutexService.register( - this.instanceMutexService.getMutexIdFor(curDb.getDbId().toHexString(), item) - ); - }); - log.info("DONE Ensuring inventory item mutexes exist for database: {}", curDb.getDbName()); - } + try { + log.info("Ensuring inventory item mutexes exist."); - log.info("DONE Ensuring inventory item mutexes exist."); + for (DbCacheEntry curDb : this.oqmDatabaseService.getDatabases()) { + log.info("Ensuring inventory item mutexes exist for database: {}", curDb.getDbName()); + this.inventoryItemService.iterator(curDb.getDbId().toHexString()).forEachRemaining((item) -> { + this.instanceMutexService.register(this.instanceMutexService.getMutexIdFor(curDb.getDbId().toHexString(), item)); + }); + log.info("DONE Ensuring inventory item mutexes exist for database: {}", curDb.getDbName()); + } + + log.info("DONE Ensuring inventory item mutexes exist."); + } catch (RuntimeException e) { + readinessStatus.markDown("Inventory item mutex initialization failed: " + e.getMessage()); + throw e; + } } + private void upgradeDbs(){ + //TODO:: create flag service to check if things initted right. Setup filter to check this flag to reject requests until setup done. + //TODO:: integrate into healthcheck. only DOWN if db upgrade failed + try { + Optional schemaUpgradeResult = this.objectSchemaUpgradeService.updateSchema(); + if(schemaUpgradeResult.isEmpty()){ + log.warn("Did not upgrade schema at start."); + } else { + log.info("Schema upgrade result: {}", schemaUpgradeResult.get()); + //TODO:: rescan inv update stats + } + } catch (RuntimeException e) { + readinessStatus.markDown("Database schema upgrade failed: " + e.getMessage()); + throw e; + } + } - void onStart( - @Observes - StartupEvent ev - ) { - this.initDb(); - } + private void initDbs(){ + try { + log.info("Initializing all databases."); + for(MongoService service : this.mongoServices){ + service.initDb(); + } + log.info("DONE initializing all databases."); + } catch (RuntimeException e) { + readinessStatus.markDown("Database initialization failed: " + e.getMessage()); + throw e; + } + } - void initDb() { - for(MongoService service : this.mongoServices){ - service.initDb(); - } + + @Getter + private final HealthStatus readinessStatus = new HealthStatus("Mongo DB Init"); + + void onStart(@Observes StartupEvent ev) { + readinessStatus.markDown("Startup initialization in progress"); + this.upgradeDbs(); + this.initDbs(); this.ensureItemMutexesExist(); + readinessStatus.markUp("Initial db initialization tasks finished"); + log.info("FINISHED initial db initialization tasks."); } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/ItemStatsService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/ItemStatsService.java index b7128c57ed..6f15e1d452 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/ItemStatsService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/ItemStatsService.java @@ -16,6 +16,7 @@ import tech.ebp.oqm.core.api.model.object.history.details.HistoryDetail; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.notification.processing.ItemExpiryLowStockItemProcessResults; import tech.ebp.oqm.core.api.model.object.storage.items.notification.processing.ItemPostTransactionProcessResults; import tech.ebp.oqm.core.api.model.object.storage.items.notification.processing.StoredExpiryLowStockProcessResult; @@ -62,51 +63,51 @@ @Slf4j @ApplicationScoped public class ItemStatsService { - + @Inject @Getter(AccessLevel.PRIVATE) CoreApiInteractingEntity coreApiInteractingEntity; - + @Getter @Inject InventoryItemService inventoryItemService; - + @Getter @Inject StoredService storedService; - + // private void addToStats(InventoryItem item, BasicStatsContaining statsToAddTo, Stored stored) { statsToAddTo.setNumStored(statsToAddTo.getNumStored() + 1L); - + if (stored.getType() == StoredType.AMOUNT) { AmountStored amtStored = (AmountStored) stored; if (amtStored.getLowStockThreshold() != null && stored.getNotificationStatus().isLowStock()) { statsToAddTo.setNumLowStock(statsToAddTo.getNumLowStock() + 1L); } } - + if (stored.getNotificationStatus().isExpiredWarning()) { statsToAddTo.setNumExpiryWarn(statsToAddTo.getNumExpiryWarn() + 1L); } - + if (stored.getNotificationStatus().isExpired()) { statsToAddTo.setNumExpired(statsToAddTo.getNumExpired() + 1L); } - + /* * Prices */ stored.applyDefaultsFromItem(item);//TODO:: this might want to happen earlier //add each to stats - for(CalculatedPricing calcedPricing : stored.getCalculatedPrices()){ + for (CalculatedPricing calcedPricing : stored.getCalculatedPrices()) { Optional existingPricing = statsToAddTo.getPrices().stream() - .filter((price)->{ - return price.getLabel().equals(calcedPricing.getLabel()); - }) - .findFirst(); - - if(existingPricing.isEmpty()){ + .filter((price)->{ + return price.getLabel().equals(calcedPricing.getLabel()); + }) + .findFirst(); + + if (existingPricing.isEmpty()) { statsToAddTo.getPrices().add( TotalPricing.builder() .label(calcedPricing.getLabel()) @@ -118,10 +119,10 @@ private void addToStats(InventoryItem item, BasicStatsContaining statsToAddTo, S } } } - + private void addToStats(InventoryItem item, StatsWithTotalContaining statsToAddTo, Stored stored) { this.addToStats(item, (BasicStatsContaining) statsToAddTo, stored); - + Quantity toAdd = switch (stored.getType()) { case AMOUNT -> { AmountStored amountStored = (AmountStored) stored; @@ -129,83 +130,83 @@ private void addToStats(InventoryItem item, StatsWithTotalContaining statsToAddT } case UNIQUE -> UnitUtils.Quantities.UNIT_ONE; }; - + statsToAddTo.setTotal( statsToAddTo.getTotal().add(toAdd) ); } - + private void addToStats(InventoryItem item, StoredInBlockStats storedInBlockStats, Stored stored) { storedInBlockStats.setHasStored(true); this.addToStats(item, (StatsWithTotalContaining) storedInBlockStats, stored); } - + private void addToStats(InventoryItem item, ItemStoredStats itemStoredStats, Stored stored) { StoredInBlockStats storedInBlockStats = itemStoredStats.getStorageBlockStats().get( - ((StoredInBlock)stored.getState()).getStorageBlock() + ((StoredInBlock) stored.getState()).getStorageBlock() ); - + this.addToStats(item, storedInBlockStats, stored); this.addToStats(item, (StatsWithTotalContaining) itemStoredStats, stored); } - + //TODO:: wtf is this -// private void addToStats(String oqmDbIdOrName, ClientSession cs, StoredStats storedStats, Stored stored) { -// -// if (!storedStats.getItemStats().containsKey(stored.getId())) { -// storedStats.getItemStats().put( -// stored.getId(), new ItemStoredStats( -// this.inventoryItemService.get(oqmDbIdOrName, cs, stored.getItem()).getUnit() -// ) -// ); -// } -// ItemStoredStats itemStoredStats = storedStats.getItemStats().get(stored.getItem()); -// -// this.addToStats((BasicStatsContaining) storedStats, stored); -// this.addToStats(itemStoredStats, stored); -// } -// public StoredStats getStoredStats(String oqmDbIdOrName, ClientSession cs, StoredSearch search) { -// FindIterable storedInItem = this.getStoredService().listIterator(oqmDbIdOrName, cs, search); -// StoredStats output = new StoredStats(); -// -// try ( -// MongoCursor storedIterator = storedInItem.iterator() -// ) { -// while (storedIterator.hasNext()) { -// Stored curStored = storedIterator.next(); -// -// this.addToStats( -// oqmDbIdOrName, -// cs, -// output, -// curStored -// ); -// } -// } -// -// return output; -// } - + // private void addToStats(String oqmDbIdOrName, ClientSession cs, StoredStats storedStats, Stored stored) { + // + // if (!storedStats.getItemStats().containsKey(stored.getId())) { + // storedStats.getItemStats().put( + // stored.getId(), new ItemStoredStats( + // this.inventoryItemService.get(oqmDbIdOrName, cs, stored.getItem()).getUnit() + // ) + // ); + // } + // ItemStoredStats itemStoredStats = storedStats.getItemStats().get(stored.getItem()); + // + // this.addToStats((BasicStatsContaining) storedStats, stored); + // this.addToStats(itemStoredStats, stored); + // } + // public StoredStats getStoredStats(String oqmDbIdOrName, ClientSession cs, StoredSearch search) { + // FindIterable storedInItem = this.getStoredService().listIterator(oqmDbIdOrName, cs, search); + // StoredStats output = new StoredStats(); + // + // try ( + // MongoCursor storedIterator = storedInItem.iterator() + // ) { + // while (storedIterator.hasNext()) { + // Stored curStored = storedIterator.next(); + // + // this.addToStats( + // oqmDbIdOrName, + // cs, + // output, + // curStored + // ); + // } + // } + // + // return output; + // } + // - + @WithSpan public ItemStoredStats getItemStats(String oqmDbIdOrName, ClientSession cs, InventoryItem item) { log.info("Getting stats for item: {}", item.getId()); - + ItemStoredStats output = new ItemStoredStats(item.getUnit(), item.getDefaultPrices()); - - for (ObjectId storageBlock : item.getStorageBlocks()) { + + for (ObjectId storageBlock : item.getStorageBlockIds().toList()) { output.getStorageBlockStats().put(storageBlock, new StoredInBlockStats(output.getTotal().getUnit())); } - - if(item.getId() != null) { + + if (item.getId() != null) { FindIterable storedInItem = this.getStoredService().listIterator(oqmDbIdOrName, cs, new StoredSearch().setInventoryItemId(item.getId())); try ( MongoCursor storedIterator = storedInItem.iterator() ) { while (storedIterator.hasNext()) { Stored curStored = storedIterator.next(); - + this.addToStats( item, output, @@ -215,18 +216,19 @@ public ItemStoredStats getItemStats(String oqmDbIdOrName, ClientSession cs, Inve } } log.info("Finished getting stats for item: {}", item.getId()); - + //TODO::: process low stock + return output; } - + public ItemStoredStats getItemStats(String oqmDbIdOrName, ClientSession cs, ObjectId itemId) { InventoryItem item = this.inventoryItemService.get(oqmDbIdOrName, cs, itemId); return this.getItemStats(oqmDbIdOrName, cs, item); } - + /** * Processes a stored item to determine if expired/expiring or low stock. - * + *

* If flags changed, updates stored object in database, with history details passed. * * @param oqmDbIdOrName @@ -237,6 +239,7 @@ public ItemStoredStats getItemStats(String oqmDbIdOrName, ClientSession cs, Obje * @param checkExpired * @param entity * @param historyDetails + * * @return Empty if nothing changed. Result if any state changed. */ private Optional getStoredExpiryLowStockProcessResult( @@ -251,10 +254,10 @@ private Optional getStoredExpiryLowStockProce ) { StoredExpiryLowStockProcessResult curResult = StoredExpiryLowStockProcessResult.builder().storedId(stored.getId()).build(); boolean changed = false; - + if (checkLowStock && stored.getType() == StoredType.AMOUNT) { AmountStored amountStored = (AmountStored) stored; - + if (UnitUtils.atOrUnderThreshold(amountStored.getLowStockThreshold(), amountStored.getAmount())) { curResult.setLowStock(true); if (!amountStored.getNotificationStatus().isLowStock()) { @@ -269,7 +272,7 @@ private Optional getStoredExpiryLowStockProce } } } - + if (checkExpired && stored.getExpires() != null) { ZonedDateTime now = ZonedDateTime.now(); if ( @@ -286,7 +289,7 @@ private Optional getStoredExpiryLowStockProce } } else if ( !expiryWarningThreshold.equals(Duration.ZERO) && - now.isAfter(stored.getExpires().minus(expiryWarningThreshold)) + now.isAfter(stored.getExpires().minus(expiryWarningThreshold)) ) { curResult.setExpired(false); curResult.setExpiryWarn(true); @@ -296,27 +299,27 @@ private Optional getStoredExpiryLowStockProce changed = true; } } else { - if(stored.getNotificationStatus().isExpiredWarning()){ + if (stored.getNotificationStatus().isExpiredWarning()) { stored.getNotificationStatus().setExpiredWarning(false); changed = true; } - if(stored.getNotificationStatus().isExpired()){ + if (stored.getNotificationStatus().isExpired()) { stored.getNotificationStatus().setExpired(false); changed = true; } } } - + if (changed) { this.getStoredService().update(oqmDbIdOrName, cs, stored, entity, true, historyDetails); return Optional.of(curResult); } return Optional.empty(); } - + /** * Performs the necessary processing to recalculate stats about an item after an item has a transaction applied. - * + *

* If stats changed, updated inventory item. * * @param oqmDbIdOrName @@ -340,10 +343,10 @@ public ItemPostTransactionProcessResults postTransactionProcess( HistoryDetail... historyDetails ) { //TODO:: apply mutex here? - + Set concerningIds = concerning.stream().map(Stored::getId).collect(Collectors.toSet()); //TODO:: separate thread to get these stats - + //process expiry and low stock for affected stored ItemExpiryLowStockItemProcessResults results = new ItemExpiryLowStockItemProcessResults().setItem(item.getId()); { @@ -353,7 +356,7 @@ oqmDbIdOrName, cs, new StoredSearch() .setInStorageBlocks( concerning.stream() .filter(s->s.isState(StoredStateType.STORED)) - .map(s->((StoredInBlock)(s.getState())).getStorageBlock()).distinct().collect(Collectors.toList()) + .map(s->((StoredInBlock) (s.getState())).getStorageBlock()).distinct().collect(Collectors.toList()) ) ); try ( @@ -361,7 +364,7 @@ oqmDbIdOrName, cs, new StoredSearch() ) { while (storedIterator.hasNext()) { Stored curStored = storedIterator.next(); - + Optional result = this.getStoredExpiryLowStockProcessResult( oqmDbIdOrName, cs, @@ -374,8 +377,8 @@ oqmDbIdOrName, cs, new StoredSearch() ); if (result.isPresent()) { StoredExpiryLowStockProcessResult curResult = result.get(); - ObjectId block = ((StoredInBlock)curStored.getState()).getStorageBlock(); - + ObjectId block = ((StoredInBlock) curStored.getState()).getStorageBlock(); + if (!results.getResults().containsKey(block)) { results.getResults().put(block, new ArrayList<>()); } @@ -384,13 +387,13 @@ oqmDbIdOrName, cs, new StoredSearch() } } } - + ItemStoredStats oldStats = item.getStats(); ItemStoredStats storedStats = this.getItemStats(oqmDbIdOrName, cs, item.getId()); item.setStats(storedStats); - + boolean changed = false; - + if (!storedStats.equals(oldStats)) { changed = true; } @@ -398,31 +401,54 @@ oqmDbIdOrName, cs, new StoredSearch() if (UnitUtils.atOrUnderThreshold(item.getLowStockThreshold(), storedStats.getTotal())) { results.setLowStock(true); storedStats.setLowStock(true); - + if (!oldStats.isLowStock()) { changed = true; } - - if(!item.getNotificationStatus().isLowStock()) { + + if (!item.getNotificationStatus().isLowStock()) { item.getNotificationStatus().setLowStock(true); //TODO:: handle notification } } else { item.getNotificationStatus().setLowStock(false); storedStats.setLowStock(false); - item.getNotificationStatus().setLowStock(false); - + if (oldStats.isLowStock()) { changed = true; } } } - + for (StorageBlockSettings blockSettings : item.getStorageBlocks().stream().filter(StorageBlockSettings::hasLowStockThreshold).toList()) { + StoredInBlockStats blockStats = storedStats.getStorageBlockStats().get(blockSettings.getStorageBlock()); + StoredInBlockStats oldBlockStats = oldStats.getStorageBlockStats().get(blockSettings.getStorageBlock()); + + if (UnitUtils.atOrUnderThreshold(blockSettings.getLowStockThreshold(), blockStats.getTotal())) { + blockStats.setLowStock(true); + + if (oldBlockStats == null || !oldBlockStats.isLowStock()) { + changed = true; + } + + if (!blockSettings.getNotificationStatus().isLowStock()) { + blockSettings.getNotificationStatus().setLowStock(true); + //TODO:: handle notification + } + } else { + blockSettings.getNotificationStatus().setLowStock(false); + blockStats.setLowStock(false); + + if (oldBlockStats == null || oldBlockStats.isLowStock()) { + changed = true; + } + } + } + List inBlockResults = results.getResults().values().stream().flatMap(List::stream).toList(); if (!inBlockResults.isEmpty()) { changed = true; } - + if (changed) { this.getInventoryItemService().update(oqmDbIdOrName, cs, item, entity, true, historyDetails); results.getEvents(transactionId).parallelStream().forEach(event->{ @@ -432,18 +458,20 @@ oqmDbIdOrName, cs, new StoredSearch() this.getStoredService().addHistoryFor(oqmDbIdOrName, cs, event.getObjectId(), this.getCoreApiInteractingEntity(), event); } }); - + } - + return ItemPostTransactionProcessResults.builder() .expiryLowStockResults(results) .stats(storedStats) .build(); } - + /** * Scans the entire given database for expired items. + * * @param oqmDbIdOrName The id or name of the database to scan. + * * @return The number of new */ public long scanForExpired(String oqmDbIdOrName) { @@ -461,14 +489,14 @@ public long scanForExpired(String oqmDbIdOrName) { long output = 0L; while (storedIterator.hasNext()) { Stored curStored = storedIterator.next(); - + if (!itemExpiryWarningThresholds.containsKey(curStored.getItem())) { itemExpiryWarningThresholds.put( curStored.getItem(), this.inventoryItemService.get(oqmDbIdOrName, curStored.getItem()).getExpiryWarningThreshold() ); } - + Optional result = this.getStoredExpiryLowStockProcessResult( oqmDbIdOrName, csw.getClientSession(), @@ -480,7 +508,7 @@ public long scanForExpired(String oqmDbIdOrName) { ); if (result.isPresent()) { StoredExpiryLowStockProcessResult curResult = result.get(); - + for ( ObjectHistoryEvent curEvent : curResult.getEvents(null, null)//Shouldn't hit the code that uses these parameters @@ -495,5 +523,5 @@ public long scanForExpired(String oqmDbIdOrName) { } } } - + } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/CustomUnitService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/CustomUnitService.java index 00042eb256..a6602cc724 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/CustomUnitService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/CustomUnitService.java @@ -7,15 +7,18 @@ import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.validation.Valid; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bson.BsonDocument; import org.bson.types.ObjectId; +import tech.ebp.oqm.core.api.exception.db.DbNotFoundException; +import tech.ebp.oqm.core.api.health.utils.HasReadinessCheck; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; import tech.ebp.oqm.core.api.model.collectionStats.CollectionStats; +import tech.ebp.oqm.core.api.model.rest.search.CustomUnitSearch; import tech.ebp.oqm.core.api.model.rest.unit.custom.NewCustomUnitRequest; import tech.ebp.oqm.core.api.model.units.CustomUnitEntry; import tech.ebp.oqm.core.api.model.units.UnitUtils; -import tech.ebp.oqm.core.api.model.rest.search.CustomUnitSearch; -import tech.ebp.oqm.core.api.exception.db.DbNotFoundException; import javax.measure.Unit; import java.util.ArrayList; @@ -26,94 +29,101 @@ */ @Slf4j @ApplicationScoped -public class CustomUnitService extends TopLevelMongoService { - - CustomUnitService() { - super(CustomUnitEntry.class); - } - - @PostConstruct - void readInUnits() { - log.info("Reading existing custom units from database..."); - - try ( - MongoCursor it = this.listIterator(null, Sorts.ascending("order"), null) - .batchSize(1) - .iterator() - ) { - while (it.hasNext()) { - CustomUnitEntry curEntry = it.next(); - log.debug("Registering unit {}", curEntry); - UnitUtils.registerAllUnits(curEntry); - } - } - log.info("Done reading in custom units."); - } - - public long getNextOrderValue() { - CustomUnitEntry entry = this.listIterator(null, Sorts.descending("order"), null).first(); - - if (entry == null) { - return 0; - } - return entry.getOrder() + 1L; - } - - public CustomUnitEntry getFromUnit(ClientSession clientSession, Unit unit) { - List matchList = this.listIterator( - clientSession, - Filters.eq("unitCreator.symbol", unit.getSymbol()), - null, - null - ).into(new ArrayList<>()); - - if (matchList.isEmpty()) { - throw new DbNotFoundException("Could not find custom unit " + unit, CustomUnitEntry.class); - } - if (matchList.size() != 1) { - throw new DbNotFoundException( - "Could not find custom unit " + unit + " - Too many matched units (" + matchList.size() + ")", - CustomUnitEntry.class - ); - } - - return matchList.get(0); - } - - public ObjectId add(ClientSession cs, @Valid CustomUnitEntry entry){ - log.info("Adding new custom unit."); - - UnitUtils.registerAllUnits(entry); - - ObjectId id = null; - if(cs == null){ - id = this.getTypedCollection().insertOne(entry).getInsertedId().asObjectId().getValue(); - } else { - id = this.getTypedCollection().insertOne(cs, entry).getInsertedId().asObjectId().getValue(); - } - entry.setId(id); - - log.info("New custom unit: {}", entry); - return entry.getId(); - } - - public ObjectId add(ClientSession cs, @Valid NewCustomUnitRequest ncur){ - log.info("Adding new custom unit."); - CustomUnitEntry newUnit = ncur.toCustomUnitEntry(this.getNextOrderValue()); - - return this.add(cs, newUnit); - } - - public List list(){ - return this.listIterator(null, Sorts.ascending("order"), null).into(new ArrayList<>()); - } - - public void removeAll(){ - this.getTypedCollection().deleteMany(new BsonDocument()); - } - - @Override - public int getCurrentSchemaVersion() { - return CustomUnitEntry.CUR_SCHEMA_VERSION; - } +public class CustomUnitService extends TopLevelMongoService implements HasReadinessCheck { + + CustomUnitService() { + super(CustomUnitEntry.class); + } + + @Getter + private final HealthStatus readinessStatus = new HealthStatus("Custom Unit Service"); + + @PostConstruct + void readInUnits() { + log.info("Reading existing custom units from database..."); + try ( + MongoCursor it = this.listIterator(null, Sorts.ascending("order"), null) + .batchSize(1) + .iterator() + ) { + while (it.hasNext()) { + CustomUnitEntry curEntry = it.next(); + log.debug("Registering unit {}", curEntry); + UnitUtils.registerAllUnits(curEntry); + } + } catch (Exception e) { + readinessStatus.markDown("Error occurred while reading custom units."); + log.error("Error occurred while reading in custom units from database.", e); + throw e; + } + log.info("Done reading in custom units."); + readinessStatus.markUp("Custom units read in and registered"); + } + + public long getNextOrderValue() { + CustomUnitEntry entry = this.listIterator(null, Sorts.descending("order"), null).first(); + + if (entry == null) { + return 0; + } + return entry.getOrder() + 1L; + } + + public CustomUnitEntry getFromUnit(ClientSession clientSession, Unit unit) { + List matchList = this.listIterator( + clientSession, + Filters.eq("unitCreator.symbol", unit.getSymbol()), + null, + null + ).into(new ArrayList<>()); + + if (matchList.isEmpty()) { + throw new DbNotFoundException("Could not find custom unit " + unit, CustomUnitEntry.class); + } + if (matchList.size() != 1) { + throw new DbNotFoundException( + "Could not find custom unit " + unit + " - Too many matched units (" + matchList.size() + ")", + CustomUnitEntry.class + ); + } + + return matchList.get(0); + } + + public ObjectId add(ClientSession cs, @Valid CustomUnitEntry entry) { + log.info("Adding new custom unit."); + + UnitUtils.registerAllUnits(entry); + + ObjectId id = null; + if (cs == null) { + id = this.getTypedCollection().insertOne(entry).getInsertedId().asObjectId().getValue(); + } else { + id = this.getTypedCollection().insertOne(cs, entry).getInsertedId().asObjectId().getValue(); + } + entry.setId(id); + + log.info("New custom unit: {}", entry); + return entry.getId(); + } + + public ObjectId add(ClientSession cs, @Valid NewCustomUnitRequest ncur) { + log.info("Adding new custom unit."); + CustomUnitEntry newUnit = ncur.toCustomUnitEntry(this.getNextOrderValue()); + + return this.add(cs, newUnit); + } + + public List list() { + return this.listIterator(null, Sorts.ascending("order"), null).into(new ArrayList<>()); + } + + public void removeAll() { + this.getTypedCollection().deleteMany(new BsonDocument()); + } + + @Override + public int getCurrentSchemaVersion() { + return CustomUnitEntry.CUR_SCHEMA_VERSION; + } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java index 845face858..e5bb4f76d7 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java @@ -26,6 +26,7 @@ import tech.ebp.oqm.core.api.model.object.media.file.FileAttachment; import tech.ebp.oqm.core.api.model.object.storage.ItemCategory; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.pricing.StoredPricing; import tech.ebp.oqm.core.api.model.object.storage.items.stored.stats.ItemStoredStats; import tech.ebp.oqm.core.api.model.object.storage.storageBlock.StorageBlock; @@ -52,48 +53,48 @@ @Slf4j @ApplicationScoped public class InventoryItemService extends MongoHistoriedObjectService { - + @Inject @Getter(AccessLevel.PRIVATE) CoreApiInteractingEntity coreApiInteractingEntity; - + @Inject @Getter(AccessLevel.PRIVATE) ItemCheckoutService itemCheckoutService; - + @Inject @Getter(AccessLevel.PRIVATE) StorageBlockService storageBlockService; - + @Inject @Getter(AccessLevel.PRIVATE) ItemCategoryService itemCategoryService; - + @Inject @Getter(AccessLevel.PRIVATE) StoredService storedService; - + @Inject @Getter(AccessLevel.PRIVATE) IdentifierGenerationService identifierGenerationService; - + @Inject @Getter(AccessLevel.PRIVATE) HistoryEventNotificationService hens; - + @Getter(AccessLevel.PRIVATE) @Inject ItemStatsService itemStatsService; @Inject StoredInItemEndpoints storedInItemEndpoints; - + @Inject InstanceMutexService instanceMutexService; - + public InventoryItemService() { super(InventoryItem.class, false); } - + //TODO:: this better @Override public Set getDisallowedUpdateFields() { @@ -102,11 +103,11 @@ public Set getDisallowedUpdateFields() { output.add("stats"); return output; } - + @Override public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, InventoryItem newOrChangedObject, ClientSession clientSession) throws ValidationException { super.ensureObjectValid(oqmDbIdOrName, newObject, newOrChangedObject, clientSession); - + for (ObjectId curCategoryId : newOrChangedObject.getCategories()) { try { this.getItemCategoryService().get(oqmDbIdOrName, curCategoryId); @@ -114,15 +115,15 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Inventory throw new ValidationException("Item category " + curCategoryId.toHexString() + " does not exist.", e); } } - - for (ObjectId curObjectId : newOrChangedObject.getStorageBlocks()) { + + for (StorageBlockSettings curBlockSettings : newOrChangedObject.getStorageBlocks()) { try { - this.getStorageBlockService().get(oqmDbIdOrName, curObjectId); + this.getStorageBlockService().get(oqmDbIdOrName, curBlockSettings.getStorageBlock()); } catch(DbNotFoundException e) { - throw new ValidationException("Storage block " + curObjectId.toHexString() + " does not exist.", e); + throw new ValidationException("Storage block " + curBlockSettings.getStorageBlock().toHexString() + " does not exist.", e); } } - + List nameResults = this.list(oqmDbIdOrName, eq("name", newOrChangedObject.getName()), null, null); if (!nameResults.isEmpty()) { if (newObject) { @@ -135,7 +136,7 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Inventory } } } - + //TODO:: contemplate uniqueness of id's? // for (Identifier curUniqueId : newOrChangedObject.getIdentifiers()) { // if (curUniqueId.getType() == IdentifierType.TO_GENERATE) { @@ -155,11 +156,11 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Inventory // } // } // } - + if (!newObject) { //TODO:: in try? InventoryItem existing = this.get(oqmDbIdOrName, newOrChangedObject.getId()); - + if (!existing.getUnit().isCompatible(newOrChangedObject.getUnit())) { throw new ValidationException("New unit not compatible with current unit."); } @@ -173,7 +174,7 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Inventory } } } - + @Override public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, ObjectNode updates) { try { @@ -186,17 +187,17 @@ public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, Objec log.debug("Unit changed for item {}. Recalculating stats.", item.getId()); return true; } - + if(//storage blocks updates.has("storageBlocks") && !item.getStorageBlocks().equals( - this.getObjectMapper().treeToValue(updates.get("storageBlocks"), new TypeReference>(){}) + this.getObjectMapper().treeToValue(updates.get("storageBlocks"), new TypeReference>(){}) ) ){ - log.debug("Storage Blocks changed for item {}. Recalculating stats.", item.getId()); + log.debug("Storage Blocks and/or settings changed for item {}. Recalculating stats.", item.getId()); return true; } - + if (updates.has("expiryWarningThreshold")) { if (item.getExpiryWarningThreshold() == null) { if (!updates.get("expiryWarningThreshold").isNull()) { @@ -208,7 +209,7 @@ public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, Objec log.debug("Expiry warning threshold changed for item {}. Recalculating stats.", item.getId()); return true; } - + if ( !item.getExpiryWarningThreshold().equals( Duration.of(updates.get("expiryWarningThreshold").asLong(), ChronoUnit.SECONDS) @@ -219,7 +220,7 @@ public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, Objec } } } - + if (updates.has("lowStockThreshold")) { if (item.getLowStockThreshold() == null) { if (!updates.get("lowStockThreshold").isNull()) { @@ -231,7 +232,7 @@ public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, Objec log.debug("Low Stock threshold changed for item {}. Recalculating stats.", item.getId()); return true; } - + if ( !item.getLowStockThreshold().equals( this.getObjectMapper().treeToValue(updates.get("lowStockThreshold"), Quantity.class) @@ -242,7 +243,7 @@ public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, Objec } } } - + if( updates.has("defaultPrices") && !item.getDefaultPrices().equals( @@ -255,25 +256,25 @@ public boolean needsDerivedUpdatesAfterUpdate(@NotNull InventoryItem item, Objec } catch(JsonProcessingException e) { throw new RuntimeException("Failed to process update node. This likely shouldn't happen here.", e); } - - + + return super.needsDerivedUpdatesAfterUpdate(item, updates); } - + @Override public void massageIncomingData(String oqmDbIdOrName, ClientSession session, @NonNull InventoryItem item, boolean recalculateDerived) { super.massageIncomingData(oqmDbIdOrName, session, item, recalculateDerived); - + if (recalculateDerived) { log.debug("Calculating item stats after add/update."); item.setStats(this.getItemStatsService().getItemStats(oqmDbIdOrName, session, item)); } else { log.debug("Did not calculate item stats after add/update"); } - + item.setIdentifiers(this.getIdentifierGenerationService().replaceIdPlaceholders(oqmDbIdOrName, item.getIdentifiers())); } - + @Override public InvItemCollectionStats getStats(String oqmDbIdOrName) { return super.addBaseStats(oqmDbIdOrName, InvItemCollectionStats.builder()) @@ -282,13 +283,13 @@ public InvItemCollectionStats getStats(String oqmDbIdOrName) { .numLowStock(this.getNumLowStock(oqmDbIdOrName)) .build(); } - + @Override protected void handleAdd(String oqmDbIdOrName, InventoryItem object) { super.handleAdd(oqmDbIdOrName, object); this.instanceMutexService.register(oqmDbIdOrName, object); } - + @Override public InventoryItem update(String oqmDbIdOrName, ClientSession cs, ObjectId id, ObjectNode updateJson, InteractingEntity interactingEntity, HistoryDetail ... details) { try( @@ -299,7 +300,7 @@ public InventoryItem update(String oqmDbIdOrName, ClientSession cs, ObjectId id, return super.update(oqmDbIdOrName, cs, id, updateJson, interactingEntity, details); } } - + public List getItemsInBlock(String oqmDbIdOrName, ObjectId storageBlockId) { return this.list( oqmDbIdOrName, @@ -308,19 +309,19 @@ public List getItemsInBlock(String oqmDbIdOrName, ObjectId storag null ); } - + public long getNumStoredExpired(String oqmDbIdOrName) { return this.getSumOfIntField(oqmDbIdOrName, "numExpired"); } - + public long getNumStoredExpiryWarn(String oqmDbIdOrName) { return this.getSumOfIntField(oqmDbIdOrName, "numExpiryWarn"); } - + public long getNumLowStock(String oqmDbIdOrName) { return this.getSumOfIntField(oqmDbIdOrName, "numLowStock"); } - + // public List getItemsWithIdentifier(String oqmDbIdOrName, ClientSession clientSession, Identifier id) { // Bson filter; // @@ -352,7 +353,7 @@ public long getNumLowStock(String oqmDbIdOrName) { // // return list; // } - + public Set getItemsReferencing(String oqmDbIdOrName, ClientSession clientSession, Image image) { // { "imageIds": {$elemMatch: {$eq:ObjectId('6335f3c338a79a4377aea064')}} } // https://stackoverflow.com/questions/76178393/how-to-recreate-bson-query-with-elemmatch @@ -364,27 +365,27 @@ public Set getItemsReferencing(String oqmDbIdOrName, ClientSession cli null, null ).map(InventoryItem::getId).into(list); - + return list; } - + public Set getItemsReferencing(String oqmDbIdOrName, ClientSession clientSession, StorageBlock storageBlock) { Set list = new TreeSet<>(); - + //TODO:: figure out how find with query this.listIterator(oqmDbIdOrName, clientSession).forEach((InventoryItem item)->{ - if (item.getStorageBlocks().contains(storageBlock.getId())) { + if (item.usesStorageBlock(storageBlock.getId())) { list.add(item.getId()); } }); - + return list; } - + public Set getItemsReferencing(String oqmDbIdOrName, ClientSession clientSession, ItemCategory itemCategory) { // { "imageIds": {$elemMatch: {$eq:ObjectId('6335f3c338a79a4377aea064')}} } // https://stackoverflow.com/questions/76178393/how-to-recreate-bson-query-with-elemmatch - + Set list = new TreeSet<>(); this.listIterator( oqmDbIdOrName, @@ -395,7 +396,7 @@ public Set getItemsReferencing(String oqmDbIdOrName, ClientSession cli ).map(InventoryItem::getId).into(list); return list; } - + public Set getItemsReferencing(String oqmDbIdOrName, ClientSession clientSession, FileAttachment fileAttachment) { // https://stackoverflow.com/questions/76178393/how-to-recreate-bson-query-with-elemmatch Set list = new TreeSet<>(); @@ -408,28 +409,28 @@ public Set getItemsReferencing(String oqmDbIdOrName, ClientSession cli ).map(InventoryItem::getId).into(list); return list; } - + @Override public Map> getReferencingObjects(String oqmDbIdOrName, ClientSession cs, InventoryItem item) { Map> objsWithRefs = super.getReferencingObjects(oqmDbIdOrName, cs, item); - + Set refs = this.itemCheckoutService.getItemCheckoutsReferencing(oqmDbIdOrName, cs, item); if (!refs.isEmpty()) { objsWithRefs.put(this.itemCheckoutService.getClazz().getSimpleName(), refs); } - + return objsWithRefs; } - + @Override public int getCurrentSchemaVersion() { return InventoryItem.CUR_SCHEMA_VERSION; } - + @Override public void runPostUpgrade(String oqmDbIdOrName, ClientSession cs, CollectionUpgradeResult upgradeResult) { super.runPostUpgrade(oqmDbIdOrName, cs, upgradeResult); - + // log.info("client session: {}", cs); // log.info("is ack: {}", this.getMongoClient().getWriteConcern().isAcknowledged()); //// this.getDocumentCollection(oqmDbIdOrName).getWriteConcern() @@ -437,7 +438,7 @@ public void runPostUpgrade(String oqmDbIdOrName, ClientSession cs, CollectionUpg // this.getDocumentCollection(oqmDbIdOrName).find(cs).forEach((Document doc)->{ // log.info("Inv Item: {}", doc.toJson()); // }); - + FindIterable it = this.listIterator(oqmDbIdOrName, cs); for (InventoryItem item : it) { try { diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java index 03187b0e21..823fe1a36e 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/MongoService.java @@ -1,6 +1,7 @@ package tech.ebp.oqm.core.api.service.mongo; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.MongoCommandException; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; import com.mongodb.TransactionOptions; @@ -8,6 +9,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoIterable; import com.mongodb.client.model.IndexOptions; import jakarta.inject.Inject; import jakarta.validation.Validation; @@ -16,13 +18,19 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.bson.BsonDocument; +import org.bson.Document; import org.bson.conversions.Bson; import org.eclipse.microprofile.config.inject.ConfigProperty; import tech.ebp.oqm.core.api.model.collectionStats.CollectionStats; import tech.ebp.oqm.core.api.model.object.MainObject; import tech.ebp.oqm.core.api.model.rest.search.SearchObject; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** * This is the main mongo class. It specifies top level, commonly shared utilities. @@ -32,92 +40,161 @@ */ @Slf4j public abstract class MongoService, V extends CollectionStats> { - - //TODO:: move to constructor/inject? Remove? - protected static final Validator VALIDATOR; - - static { - try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { - VALIDATOR = validatorFactory.getValidator(); - } - } - - /** - * Gets the default transaction options to use for client sessions. - * @return The default transaction options. - */ - public static TransactionOptions getDefaultTransactionOptions() { - return TransactionOptions.builder() - .readPreference(ReadPreference.primary()) - .readConcern(ReadConcern.LOCAL) - .writeConcern(WriteConcern.MAJORITY) - .build(); - } - - public ClientSession getNewClientSession(boolean startTransaction) { - ClientSession clientSession = this.getMongoClient().startSession(); - - if(startTransaction){ - clientSession.startTransaction(); - } - - return clientSession; - } - - public ClientSession getNewClientSession() { - return this.getNewClientSession(false); - } - - /** - * The default collection name to use when getting the collection. - * @param clazz The class to get the collection of - * @return The collection name to use when getting the collection. - */ - public static String getCollectionNameFromClass(Class clazz) { - return clazz.getSimpleName(); - } - - /** - * The class this collection is in charge of. Used for logging, and other fun. - */ - @Getter - protected final Class clazz; - - /** - * The MongoDb client. - */ - @Inject - @Getter(AccessLevel.PROTECTED) - MongoClient mongoClient; - - /** - * Mapper to help deal with json updates. - */ - @Inject - @Getter(AccessLevel.PROTECTED) - ObjectMapper objectMapper; - - /** - * The name of the database to access - */ - @Getter - @ConfigProperty(name = "quarkus.mongodb.database") - String databasePrefix; - - public MongoService(Class clazz) { - this.clazz = clazz; - } - - public abstract int getCurrentSchemaVersion(); - - public abstract List getDbIndexes(); - - public abstract void initDb(); - - protected static void setupIndexes(MongoCollection collection, List indexes) { - IndexOptions options = new IndexOptions().background(true); - for (Bson index : indexes) { - collection.createIndex(index, options); - } - } + + // TODO:: move to constructor/inject? Remove? + protected static final Validator VALIDATOR; + + static { + try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { + VALIDATOR = validatorFactory.getValidator(); + } + } + + /** + * Gets the default transaction options to use for client sessions. + * + * @return The default transaction options. + */ + public static TransactionOptions getDefaultTransactionOptions() { + return TransactionOptions.builder().readPreference(ReadPreference.primary()).readConcern(ReadConcern.LOCAL).writeConcern(WriteConcern.MAJORITY).build(); + } + + public ClientSession getNewClientSession(boolean startTransaction) { + ClientSession clientSession = this.getMongoClient().startSession(); + + if (startTransaction) { + clientSession.startTransaction(); + } + + return clientSession; + } + + public ClientSession getNewClientSession() { + return this.getNewClientSession(false); + } + + /** + * The default collection name to use when getting the collection. + * + * @param clazz The class to get the collection of + * @return The collection name to use when getting the collection. + */ + public static String getCollectionNameFromClass(Class clazz) { + return clazz.getSimpleName(); + } + + /** + * The class this collection is in charge of. Used for logging, and other fun. + */ + @Getter + protected final Class clazz; + + /** + * The MongoDb client. + */ + @Inject + @Getter(AccessLevel.PROTECTED) + MongoClient mongoClient; + + /** + * Mapper to help deal with json updates. + */ + @Inject + @Getter(AccessLevel.PROTECTED) + ObjectMapper objectMapper; + + /** + * The name of the database to access + */ + @Getter + @ConfigProperty(name = "quarkus.mongodb.database") + String databasePrefix; + + public MongoService(Class clazz) { + this.clazz = clazz; + } + + public abstract int getCurrentSchemaVersion(); + + public abstract List getDbIndexes(); + + public abstract void initDb(); + + /** + * Sets up indexes for the given MongoDB collection. + * All expected indexes are (re)created on each call, and if an index already exists, it is dropped + * and recreated to ensure its options are always up to date. + * Indexes no longer present in the expected list are dropped. + * The default {@code _id_} index is never dropped. + * + * @param collection the MongoDB collection to manage indexes on + * @param indexes the list of indexes that should exist on the collection + */ + protected static void setupIndexes(MongoCollection collection, List indexes) { + log.info("setting up indexes for collection {}", collection.getNamespace()); + IndexOptions options = new IndexOptions().background(true); + Map existingIndexes = getExistingIndexes(collection); + Set expectedKeys = new HashSet<>(); + + for (Bson index : indexes) { + BsonDocument expectedKey = index.toBsonDocument(BsonDocument.class, collection.getCodecRegistry()); + expectedKeys.add(expectedKey); + log.info("ensuring index with key {}", expectedKey); + if (existingIndexes.containsKey(expectedKey)) { + try { + collection.createIndex(index, options); + } catch (MongoCommandException e) { + log.warn("failed to create index with key {}, dropping and recreating index", expectedKey, e); + collection.dropIndex(existingIndexes.get(expectedKey)); + collection.createIndex(index, options); + } + } else { + collection.createIndex(index, options); + } + } + + for (Map.Entry existing : existingIndexes.entrySet()) { + if (!expectedKeys.contains(existing.getKey())) { + log.info("dropping index with key {}", existing.getKey()); + collection.dropIndex(existing.getValue()); + } + } + } + + /** + * Returns a map of existing indexes on the given collection ignoring the default _id_ index. + * Input Index: + *

+     * {@code
+     * Indexes.ascending("name")
+     * }
+     * 
+ * + * Would produce: + *
+     * {@code
+     *   Key: {"name": 1}
+     *   Value: "name_1"
+     * }
+     * 
+ * + * @param collection the MongoDB collection to read indexes from + * @return map of index key document to index name + */ + private static Map getExistingIndexes(MongoCollection collection) { + MongoIterable existingDocument = collection.listIndexes(); + Map existingIndexes = new HashMap<>(); + for (Document doc : existingDocument) { + Document keyDoc = doc.get("key", Document.class); + if (keyDoc == null) { + continue; + } + BsonDocument key = keyDoc.toBsonDocument(BsonDocument.class, collection.getCodecRegistry()); + String name = doc.getString("name"); + if (name != null && !name.equals("_id_")) { + existingIndexes.put(key, name); + } + } + return existingIndexes; + } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/StoredService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/StoredService.java index b59b0f4043..ba262b4d83 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/StoredService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/StoredService.java @@ -39,30 +39,30 @@ @Slf4j @ApplicationScoped public class StoredService extends MongoHistoriedObjectService { - + @Inject @Getter(AccessLevel.PRIVATE) CoreApiInteractingEntity coreApiInteractingEntity; - + @Inject @Getter(AccessLevel.PRIVATE) InventoryItemService inventoryItemService; - + @Inject @Getter(AccessLevel.PRIVATE) IdentifierGenerationService identifierGenerationService; - + @Inject @Getter(AccessLevel.PRIVATE) ItemCheckoutService itemCheckoutService; - + @Inject @Getter(AccessLevel.PRIVATE) HistoryEventNotificationService hens; - + @Inject InstanceMutexService instanceMutexService; - + @Override public Set getDisallowedUpdateFields() { Set output = new HashSet<>(super.getDisallowedUpdateFields()); @@ -72,33 +72,33 @@ public Set getDisallowedUpdateFields() { output.add("type"); return output; } - + public StoredService() { super(Stored.class, false); } - + @Override public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Stored newOrChangedObject, ClientSession clientSession) { super.ensureObjectValid(oqmDbIdOrName, newObject, newOrChangedObject, clientSession); - + InventoryItem item; try { item = this.inventoryItemService.get(oqmDbIdOrName, newOrChangedObject.getItem()); } catch(DbNotFoundException e) { throw new ValidationException("Item " + newOrChangedObject.getItem().toHexString() + " does not exist.", e); } - + if(newOrChangedObject.getState() == null) { throw new ValidationException("Stored item must have a stored state."); } - + switch (newOrChangedObject.getState().getType()){ case STORED: ObjectId inBlock = ((StoredInBlock)(newOrChangedObject.getState())).getStorageBlock(); - if (!item.getStorageBlocks().contains(inBlock)) { + if (!item.usesStorageBlock(inBlock)) { throw new ValidationException("Storage block " + inBlock.toHexString() + " not used to hold this item (" + item.getId() + ")."); } - + if (item.getStorageType() == BULK) { SearchResult inBlockSearch = this.search( oqmDbIdOrName, @@ -106,7 +106,7 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Stored ne .setInventoryItemId(item.getId()) .setStorageBlockId(inBlock) ); - + if (!inBlockSearch.isEmpty()) { if (inBlockSearch.getNumResults() != 1) { throw new ValidationException("More than one stored held for item of type " + item.getStorageType()); @@ -117,42 +117,42 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Stored ne } } } - - + + break; } - - + + if (item.getStorageType().storedType != newOrChangedObject.getType()) { throw new ValidationException("Stored given of type " + newOrChangedObject.getType() + " cannot be held in item of storage type" + item.getStorageType()); } - + if (item.getStorageType().storedType == StoredType.AMOUNT) { if (!item.getUnit().isCompatible(((AmountStored) newOrChangedObject).getAmount().getUnit())) { throw new ValidationException("Unit of amount must be compatible with item's unit."); } } - + if (item.getStorageType().storedType == StoredType.AMOUNT && ((AmountStored) newOrChangedObject).getLowStockThreshold() != null) { if (!item.getUnit().isCompatible(((AmountStored) newOrChangedObject).getLowStockThreshold().getUnit())) { throw new ValidationException("Unit of low stock threshold must be compatible with item's unit."); } } - + if (item.getStorageType() == UNIQUE_SINGLE) { SearchResult inItem = this.search(oqmDbIdOrName, new StoredSearch().setInventoryItemId(item.getId())); if (!inItem.isEmpty()) { if (inItem.getNumResults() != 1) { throw new ValidationException("More than one globally unique stored held"); } - + Stored stored = inItem.getResults().get(0); if (newObject || !stored.getId().equals(newOrChangedObject.getId())) { throw new ValidationException("Cannot store more than one globally unique stored item."); } } } - + // for (UniqueId curUniqueId : newOrChangedObject.getUniqueIds()) { // if (curUniqueId.getType() == UniqueIdType.TO_GENERATE) { // continue; @@ -172,45 +172,45 @@ public void ensureObjectValid(String oqmDbIdOrName, boolean newObject, Stored ne // } // } } - - + + @Override public boolean needsDerivedUpdatesAfterUpdate(@NotNull Stored stored, ObjectNode updates) { //TODO return false; } - + @Override public void massageIncomingData(String oqmDbIdOrName, ClientSession session, @NonNull Stored stored, boolean recalculateDerived) { super.massageIncomingData(oqmDbIdOrName, session, stored, recalculateDerived); - + if(recalculateDerived) { //TODO:: potentially trigger refresh of item stats #929. Doublecheck to make sure not doubling up stats calculation on transaction } - + stored.setIdentifiers(this.getIdentifierGenerationService().replaceIdPlaceholders(oqmDbIdOrName, stored.getIdentifiers())); } - + @Override public SearchResult search(String oqmDbIdOrName, ClientSession cs, @NonNull StoredSearch searchObject) { SearchResult results = super.search(oqmDbIdOrName, cs, searchObject); - + if (searchObject.getInventoryItemId() != null) { results = new ItemAwareSearchResult<>(this.getInventoryItemService().get(oqmDbIdOrName, cs, searchObject.getInventoryItemId()), results); } - + return results; } - + @Override public CollectionStats getStats(String oqmDbIdOrName) { return super.addBaseStats(oqmDbIdOrName, CollectionStats.builder()) .build(); } - + public Stored update(String oqmDbIdOrName, ClientSession cs, InventoryItem item, ObjectId id, ObjectNode updateJson, InteractingEntity interactingEntity, HistoryDetail... details) { - + try( InstanceMutexService.InstanceMutexResource mutex = this.instanceMutexService.getResource(this.instanceMutexService.getMutexIdFor(oqmDbIdOrName, InventoryItem.class, item.getId()), @@ -219,34 +219,34 @@ public Stored update(String oqmDbIdOrName, ClientSession cs, InventoryItem item, return super.update(oqmDbIdOrName, cs, id, updateJson, interactingEntity, details); } } - - + + public SearchResult getStoredForItemBlock(String oqmDbIdOrName, ClientSession cs, ObjectId itemId, ObjectId storageBlockId, Class type) { StoredSearch search = new StoredSearch() .setInventoryItemId(itemId) .setStorageBlockId(storageBlockId); - + //noinspection unchecked SearchResult result = (SearchResult) this.search( oqmDbIdOrName, cs, search ); - + if (result.isEmpty()) { throw new DbNotFoundException("No stored currently stored in this block (" + storageBlockId + ") under this item (" + itemId + ").", this.clazz); } - + return result; } - + public SearchResult getStoredForItemBlock(String oqmDbIdOrName, ClientSession cs, ObjectId itemId, ObjectId storageBlockId) { return this.getStoredForItemBlock(oqmDbIdOrName, cs, itemId, storageBlockId, Stored.class); } - + public T getSingleStoredForItemBlock(String oqmDbIdOrName, ClientSession cs, ObjectId itemId, ObjectId storageBlockId, Class type) { SearchResult result = this.getStoredForItemBlock(oqmDbIdOrName, cs, itemId, storageBlockId, type); - + if (result.getNumResults() != 1) { throw new IllegalStateException("Expected single stored in this block (" + storageBlockId @@ -256,16 +256,16 @@ public T getSingleStoredForItemBlock(String oqmDbIdOrName, Cl + result.getNumResults() + "."); } - + return result.getResults().getFirst(); } - - - + + + @Override public int getCurrentSchemaVersion() { return Stored.CUR_SCHEMA_VERSION; } - + //TODO:: get referencing.... -} \ No newline at end of file +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeService.java index bf88aff6fa..e9107205b2 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeService.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.mongodb.TransactionOptions; import com.mongodb.client.ClientSession; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; @@ -21,6 +20,8 @@ import tech.ebp.oqm.core.api.config.CoreApiInteractingEntity; import tech.ebp.oqm.core.api.exception.ClassUpgraderNotFoundException; import tech.ebp.oqm.core.api.exception.UpgradeFailedException; +import tech.ebp.oqm.core.api.health.utils.HasReadinessCheck; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; import tech.ebp.oqm.core.api.model.object.MainObject; import tech.ebp.oqm.core.api.model.object.Versionable; import tech.ebp.oqm.core.api.model.object.history.details.FromSchemaUpgradeDetail; @@ -33,7 +34,15 @@ import tech.ebp.oqm.core.api.model.object.upgrade.TotalUpgradeResult; import tech.ebp.oqm.core.api.model.object.upgrade.UpgradeCreatedObjectsResults; import tech.ebp.oqm.core.api.model.object.upgrade.UpgradeOverallCreatedObjectsResults; -import tech.ebp.oqm.core.api.service.mongo.*; +import tech.ebp.oqm.core.api.service.mongo.InteractingEntityService; +import tech.ebp.oqm.core.api.service.mongo.InventoryItemService; +import tech.ebp.oqm.core.api.service.mongo.ItemCheckoutService; +import tech.ebp.oqm.core.api.service.mongo.MongoDbAwareService; +import tech.ebp.oqm.core.api.service.mongo.MongoHistoriedObjectService; +import tech.ebp.oqm.core.api.service.mongo.MongoHistoryService; +import tech.ebp.oqm.core.api.service.mongo.StorageBlockService; +import tech.ebp.oqm.core.api.service.mongo.StoredService; +import tech.ebp.oqm.core.api.service.mongo.TopLevelMongoService; import tech.ebp.oqm.core.api.service.mongo.transactions.AppliedTransactionService; import tech.ebp.oqm.core.api.service.mongo.utils.MongoSessionWrapper; import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.ObjectSchemaUpgrader; @@ -49,7 +58,13 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -58,12 +73,11 @@ import static com.mongodb.client.model.Filters.eq; import static com.mongodb.client.model.Filters.lt; -import static com.mongodb.client.model.Filters.type; @ApplicationScoped @Slf4j -public class ObjectSchemaUpgradeService { - +public class ObjectSchemaUpgradeService implements HasReadinessCheck { + /** Map of upgraders to provide easy access to which upgraders for which object class. */ private Map, ObjectSchemaUpgrader> upgraderMap; /** The main oqm database service. */ @@ -75,13 +89,16 @@ public class ObjectSchemaUpgradeService { /** Data structure for easy grouping of services that can/ should have their schema updated at the same time, and those groups in order. */ private List>> dbAwareUpgradeGroups; private TotalUpgradeResult startupUpgradeResult = null; - + @Getter(AccessLevel.PRIVATE) CoreApiInteractingEntity coreApiInteractingEntity; - + @ConfigProperty(name = "quarkus.uuid") String instanceUuid; - + + @Getter + private final HealthStatus readinessStatus = new HealthStatus("Object Schema Upgrade Service"); + @Inject public ObjectSchemaUpgradeService( CoreApiInteractingEntity coreApiInteractingEntity, @@ -98,14 +115,14 @@ public ObjectSchemaUpgradeService( this.coreApiInteractingEntity = coreApiInteractingEntity; this.instanceUuid = instanceUuid; this.oqmDatabaseService = oqmDatabaseService; - + this.topLevelServices = new LinkedHashMap<>(); Stream.of( interactingEntityService ).forEachOrdered((service)->{ this.topLevelServices.put(service.getClazz(), service); }); - + //This insertion order here is the order of which these are each processed. this.dbAwareUpgradeGroups = List.of( List.of( @@ -134,7 +151,7 @@ public ObjectSchemaUpgradeService( return map1; } ); - + this.upgraderMap = Stream.of( new HistoryEventSchemaUpgrader(), new InteractingEntitySchemaUpgrader(), @@ -155,27 +172,27 @@ public ObjectSchemaUpgradeService( } ); } - + public Optional getStartupUpgradeResult() { return Optional.ofNullable(this.startupUpgradeResult); } - + public boolean upgradeRan() { return this.startupUpgradeResult != null; } - - + + public ObjectSchemaUpgrader getUpgrader(@NonNull Class clazz) throws ClassUpgraderNotFoundException { if (!this.upgraderMap.containsKey(clazz)) { throw new ClassUpgraderNotFoundException(clazz); } return (ObjectSchemaUpgrader) this.upgraderMap.get(clazz); } - + private void clearUpgraderMap() { this.upgraderMap = null; } - + private void processCreatedObjects( ClientSession cs, String oqmDbId, @@ -185,25 +202,25 @@ private void processCreatedObjects( ) { MongoDbAwareService service = (MongoDbAwareService) this.oqmDbServices.get(newObjClass); ObjectSchemaUpgrader upgrader = (ObjectSchemaUpgrader) this.upgraderMap.get(newObjClass); - + if (service == null) { throw new IllegalStateException("Service for class not found: " + newObjClass.getName()); } if (upgrader == null) { throw new IllegalStateException("Upgrader for class not found: " + newObjClass.getName()); } - + List createdObjs = newObjects.stream() .map((no)->upgrader.upgrade(no).getUpgradedObject()) .filter(Optional::isPresent) .map(Optional::get) .toList(); - + createdObjs.stream() .forEach((curCreated)->{ ObjectId newId = service.getTypedCollection(oqmDbId) .insertOne(cs, curCreated).getInsertedId().asObjectId().getValue(); - + if (service instanceof MongoHistoriedObjectService) { ((MongoHistoriedObjectService) service).getHistoryService() .addHistoryFor( @@ -220,7 +237,7 @@ private void processCreatedObjects( } ); } - + private void processCreatedObjects( String upgradeId, ClientSession cs, @@ -247,7 +264,7 @@ private void processCreatedObjects( ); }); } - + /** * Handles the actual upgrading of schema data in a collection. Iterates over all elements in collection, upgrading each (if necessary). * @@ -273,15 +290,15 @@ private CollectionUpgradeResult upgradeOqmCollection( ObjectSchemaUpgrader objectVersionBumper = this.getUpgrader(objectClass); outputBuilder.collectionClass(objectClass); outputBuilder.collectionName(documentCollection.getNamespace().getCollectionName()); - + UpgradeOverallCreatedObjectsResults createdObjectResults = new UpgradeOverallCreatedObjectsResults(); outputBuilder.createdObjects(createdObjectResults); - + StopWatch sw = StopWatch.createStarted(); long numUpdated = 0; long numNotUpgraded = 0; long numDeleted = 0; - + if (objectVersionBumper.upgradesAvailable()) { try ( MongoCursor it = documentCollection @@ -301,7 +318,7 @@ private CollectionUpgradeResult upgradeOqmCollection( upgradedObject.map(Versionable::getSchemaVersion).orElse(-1) ) .build(); - + if (result.isDelObj()) { log.info("Deleting object with id {} in collection {}", doc.getObjectId("_id"), documentCollection.getNamespace().getCollectionName()); typedCollection.deleteOne(cs, eq("_id", result.getObjectId())); @@ -310,8 +327,8 @@ private CollectionUpgradeResult upgradeOqmCollection( numNotUpgraded++; } else { numUpdated++; - - + + log.info("Updating object db entry with id {} in collection {}", doc.getObjectId("_id"), documentCollection.getNamespace().getCollectionName()); T previous = typedCollection.findOneAndReplace( cs, @@ -324,7 +341,7 @@ private CollectionUpgradeResult upgradeOqmCollection( if (previous == null) { throw new RuntimeException("Previous object was not upgraded..."); } - + //TODO:: support top level collections to do these things if (oqmDbId != null) { //add upgrade event, if applicable @@ -341,7 +358,7 @@ private CollectionUpgradeResult upgradeOqmCollection( } } } - + //TODO:: support top level collections to do these things if (oqmDbId != null) { if (result.hasUpgradedCreatedObjects()) { @@ -362,16 +379,16 @@ private CollectionUpgradeResult upgradeOqmCollection( throw new RuntimeException(e); } } - + sw.stop(); outputBuilder.timeTaken(Duration.of(sw.getTime(TimeUnit.MILLISECONDS), ChronoUnit.MILLIS)) .numObjectsUpgraded(numUpdated) .numObjectsNotUpgraded(numNotUpgraded) .numObjectsDeleted(numDeleted); - + return outputBuilder.build(); } - + /** * Handles upgrading a particular collection. Wrapper for the other method, getting specific details from the mongo service. * @@ -402,7 +419,7 @@ private CollectionUpgradeResult upgradeOqmCollection(Stri outputBuilder ); }); - + Optional> histCollOp = Optional.empty(); if (historiedService) { log.info("Service is historied, processing history events."); @@ -422,20 +439,20 @@ private CollectionUpgradeResult upgradeOqmCollection(Stri }) ); } - + collectionFuture.get(); - + if (histCollOp.isPresent()) { ((HistoriedCollectionUpgradeResult.HistoriedCollectionUpgradeResultBuilder) outputBuilder).historyCollectionUpgradeResult(histCollOp.get().get()); } - + log.info("DONE Updating schema of oqm database service {} in ", service.getClass()); return outputBuilder.build(); } - + private CollectionUpgradeResult upgradeOqmCollection(String upgradeId, ClientSession dbCs, TopLevelMongoService service) { log.info("Updating schema of top level oqm database service {}", service.getClass()); - + CollectionUpgradeResult result = this.upgradeOqmCollection( upgradeId, dbCs, @@ -446,11 +463,11 @@ private CollectionUpgradeResult upgradeOqmCollection(Stri service.getClazz(), CollectionUpgradeResult.builder() ); - + log.info("DONE Updating schema of oqm database service {} in ", service.getClass()); return result; } - + /** * This method upgrades a particular oqm db to the latest schema. *

@@ -466,7 +483,7 @@ private OqmDbUpgradeResult upgradeOqmDb(String upgradeId, OqmMongoDatabase oqmDb .dbName(oqmDb.getName()); List upgradeResults = new ArrayList<>(); outputBuilder.collectionUpgradeResults(upgradeResults); - + StopWatch dbUpgradeTime = StopWatch.createStarted(); ClientSession cs = null; try { @@ -499,15 +516,15 @@ private OqmDbUpgradeResult upgradeOqmDb(String upgradeId, OqmMongoDatabase oqmDb cs.close(); } } - + dbUpgradeTime.stop(); outputBuilder.timeTaken(Duration.of(dbUpgradeTime.getTime(TimeUnit.MILLISECONDS), ChronoUnit.MILLIS)); - + log.info("Done updating oqm database: {}", oqmDb); - + return outputBuilder.build(); } - + /** * This method is responsible for upgrading all the collections/ databases handled by the core api. *

@@ -524,8 +541,8 @@ public Optional updateSchema(boolean force) { } final String upgradeId = UUID.randomUUID().toString(); log.info("Upgrading the schema held in the Database. Id: {}", upgradeId); - - + + AtomicReference result = new AtomicReference<>(); try (MongoSessionWrapper csw = new MongoSessionWrapper(this.oqmDatabaseService)) { csw.runTransaction(true, (ClientSession cs)->{ @@ -536,17 +553,17 @@ public Optional updateSchema(boolean force) { {//top level migration log.info("Upgrading top level collections."); List topLevelResults = new ArrayList<>(); - + //TODO:: session wrapper for all things not just one then the other for (TopLevelMongoService curTopLevelService : this.topLevelServices.values()) { topLevelResults.add(this.upgradeOqmCollection(upgradeId, cs, curTopLevelService)); } - - + + totalResultBuilder.topLevelUpgradeResults(topLevelResults); log.info("DONE upgrading top level results."); } - + List> resultMap = new ArrayList<>(); for (OqmMongoDatabase curDb : this.oqmDatabaseService.listIterator()) { resultMap.add(CompletableFuture.supplyAsync(()->{ @@ -563,18 +580,18 @@ public Optional updateSchema(boolean force) { } }) .toList()); - + cs.commitTransaction(); - + totalTime.stop(); totalResultBuilder.timeTaken(totalTime.getDuration()); result.set(totalResultBuilder.build()); - + log.info("DONE upgrading the schema held in the Database."); log.info("Running post upgrade tasks."); - + cs.startTransaction(); - + TotalUpgradeResult innerResult = result.get(); if (innerResult.wasUpgraded()) { innerResult.getTopLevelUpgradeResults().stream() @@ -593,27 +610,32 @@ public Optional updateSchema(boolean force) { log.info("Running post upgrade tasks for collection: {} / {}", curResult.getCollectionName(), curResult.getCollectionClass()); MongoDbAwareService service = this.oqmDbServices.get(curResult.getCollectionClass()); service.runPostUpgrade(curDbResult.getDbName(), cs, curResult); - + if (service instanceof MongoHistoriedObjectService) { ((MongoHistoriedObjectService) service).getHistoryService().runPostUpgrade(curDbResult.getDbName(), cs, curResult); } }); }); - + } else { log.info("No object upgraded, no reason to run post upgrade tasks."); } - + log.info("DONE running post-upgrade tasks."); }); - } - + } catch(Exception e) { + readinessStatus.markDown("Schema upgrade failed with id: " + upgradeId + ". Error: " + e.getMessage()); + log.error("Failed to upgrade schema with id: " + upgradeId, e); + throw new UpgradeFailedException("Failed to upgrade schema with id: " + upgradeId, e); + } + this.startupUpgradeResult = result.get(); - + readinessStatus.markUp("Schema upgrade completed with id: " + upgradeId); + log.info("DONE running post-upgrade tasks."); return this.getStartupUpgradeResult(); } - + public Optional updateSchema() { return this.updateSchema(false); } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/ObjectSchemaUpgrader.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/ObjectSchemaUpgrader.java index 17368a73a9..d406d26838 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/ObjectSchemaUpgrader.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/ObjectSchemaUpgrader.java @@ -29,12 +29,12 @@ */ @Slf4j public abstract class ObjectSchemaUpgrader { - + /** * Have our own here to be more lenient on unknown properties */ private static final ObjectMapper MAPPER = new ObjectMapper(); - + static { ObjectUtils.setupObjectMapper(MAPPER); MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); @@ -94,7 +94,7 @@ protected Iterator> getBumperIteratorAtVersion(int return bumpers.iterator(); } - + /** * Performs minor tweaks to the resulting upgraded object json. * @@ -107,7 +107,7 @@ protected ObjectNode adjustUpgradedObj(ObjectNode oldObj){ } return oldObj; } - + private ObjectId getObjectId(ObjectNode oldObj){ if(oldObj.has("id")) { return new ObjectId(oldObj.get("id").asText()); @@ -122,14 +122,14 @@ private ObjectId getObjectId(ObjectNode oldObj){ public ObjectUpgradeResult upgrade(ObjectNode oldObj){ int curVersion = oldObj.get("schemaVersion").asInt(1); - ObjectUpgradeResult.ObjectUpgradeResultBuilder resultBuilder = ObjectUpgradeResult.builder(); + ObjectUpgradeResult.ObjectUpgradeResultBuilder resultBuilder = (ObjectUpgradeResult.ObjectUpgradeResultBuilder) ObjectUpgradeResult.builder(); resultBuilder.objectId(this.getObjectId(oldObj)); resultBuilder.oldVersion(curVersion); UpgradeCreatedObjectsResults upgradeCreatedObjects = new UpgradeCreatedObjectsResults(); resultBuilder.upgradeCreatedObjects(upgradeCreatedObjects); ObjectNode upgradedJson = oldObj.deepCopy(); - + log.debug("Initial object (version {}): {}", curVersion, upgradedJson); //Iterate and process upgrades for each necessary bump @@ -142,10 +142,10 @@ public ObjectUpgradeResult upgrade(ObjectNode oldObj){ //Process bump SingleUpgradeResult upgradeResult = curBumper.bumpObject(upgradedJson); upgradedJson = upgradeResult.getUpgradedObject(); - + //Process created objects during the bump upgradeCreatedObjects.addAll(upgradeResult.getCreatedObjects()); - + if(upgradeResult.isDelObj()){ log.debug("Object was deleted during upgrade to schema version {}", curBumper.getBumperTo()); delObj = true; @@ -153,11 +153,11 @@ public ObjectUpgradeResult upgrade(ObjectNode oldObj){ break; } } - + this.adjustUpgradedObj(upgradedJson); - + log.debug("Upgraded object: {}", upgradedJson); - + if(delObj){ resultBuilder.upgradedObject(Optional.empty()); } else { @@ -173,7 +173,7 @@ public ObjectUpgradeResult upgrade(ObjectNode oldObj){ resultBuilder.upgradedObject(Optional.of(upgradedObj)); } sw.stop(); - + resultBuilder.timeTaken(sw.getDuration()); return resultBuilder.build(); @@ -186,13 +186,13 @@ public ObjectUpgradeResult upgrade(Document oldObjDoc) throws JsonProcessingE .build() ) ); - + if(oldObj.has("_id")) { oldObj.put("id", oldObj.get("_id").get("$oid").asText()); oldObj.remove("_id"); } oldObj.remove("storedType_mongo"); - + return this.upgrade(oldObj); } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/InventoryItemSchemaUpgrader.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/InventoryItemSchemaUpgrader.java index a6d080ff53..0e4ece9f64 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/InventoryItemSchemaUpgrader.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/InventoryItemSchemaUpgrader.java @@ -5,6 +5,7 @@ import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.inventoryItem.bumpers.InvItemBumper2; import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.inventoryItem.bumpers.InvItemBumper3; import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.inventoryItem.bumpers.InvItemBumper4; +import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.inventoryItem.bumpers.InvItemBumper5; import java.util.TreeSet; @@ -18,7 +19,8 @@ public InventoryItemSchemaUpgrader() { InventoryItem.class, new InvItemBumper2(), new InvItemBumper3(), - new InvItemBumper4() + new InvItemBumper4(), + new InvItemBumper5() ); } } diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/bumpers/InvItemBumper5.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/bumpers/InvItemBumper5.java new file mode 100644 index 0000000000..f73e9300da --- /dev/null +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/schemaVersioning/upgraders/inventoryItem/bumpers/InvItemBumper5.java @@ -0,0 +1,73 @@ +package tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.inventoryItem.bumpers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.upgrade.SingleUpgradeResult; +import tech.ebp.oqm.core.api.model.object.upgrade.UpgradeCreatedObjectsResults; +import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.ObjectSchemaVersionBumper; +import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.UpgradingUtils; +import tech.ebp.oqm.core.api.service.schemaVersioning.upgraders.sharedOps.GenUnIdToIdentifierUpgrade; + +import javax.measure.Quantity; +import javax.measure.Unit; +import java.time.Duration; + +import static tech.ebp.oqm.core.api.model.object.ObjectUtils.OBJECT_MAPPER; + +@Slf4j +public class InvItemBumper5 extends ObjectSchemaVersionBumper { + + public InvItemBumper5() { + super(5); + } + + + @Override + protected SingleUpgradeResult bumpObjectSchema(ObjectNode oldObj) { + UpgradeCreatedObjectsResults createdObjectsResults = new UpgradeCreatedObjectsResults(); + SingleUpgradeResult.SingleUpgradeResultBuilder resultBuilder = SingleUpgradeResult.builder() + .upgradedObject(oldObj) + .createdObjects(createdObjectsResults); + + UpgradingUtils.normalizeObjectIdList((ArrayNode) oldObj.get("storageBlocks")); + ArrayNode oldBlockList = (ArrayNode) oldObj.get("storageBlocks"); + ArrayNode newBlockList = oldObj.putArray("storageBlocks"); + + for(JsonNode curBlock : oldBlockList){ + log.info("DEBUG:: Adding new settings for storage block: {}", curBlock); + ObjectNode newSettings = newBlockList.addObject(); + newSettings.set("storageBlock", curBlock); + } + + //update existing fields + UpgradingUtils.stringToConvertedTree(oldObj, "expiryWarningThreshold", Duration.class); + UpgradingUtils.stringToConvertedTree(oldObj, "unit", Unit.class); + UpgradingUtils.stringToConvertedTree(oldObj, "lowStockThreshold", Quantity.class); + UpgradingUtils.normalizeObjectIdList((ArrayNode) oldObj.get("storageBlocks")); + UpgradingUtils.normalizeObjectIdList((ArrayNode) oldObj.get("imageIds")); + UpgradingUtils.normalizeObjectIdList((ArrayNode) oldObj.get("attachedFiles")); + UpgradingUtils.normalizeObjectIdList((ArrayNode) oldObj.get("categories")); + UpgradingUtils.normalizeObjectIdList((ArrayNode) oldObj.get("idGenerators")); + + + if(oldObj.has("stats") && !oldObj.get("stats").isEmpty()){ + UpgradingUtils.stringToConvertedTree((ObjectNode) oldObj.get("stats"), "total", Quantity.class); + + for(JsonNode curBlockStats : oldObj.get("stats").get("storageBlockStats")){ + UpgradingUtils.stringToConvertedTree((ObjectNode) curBlockStats, "total", Quantity.class); + } + } + + UpgradingUtils.monetaryAmountMongoToJackson((ArrayNode) oldObj.get("defaultPrices")); + UpgradingUtils.monetaryAmountMongoToJackson((ArrayNode) oldObj.get("stats").get("prices")); + + for(JsonNode curBlockStat : oldObj.get("stats").get("storageBlockStats")){ + UpgradingUtils.monetaryAmountMongoToJackson((ArrayNode) curBlockStat.get("prices")); + } + + return resultBuilder.build(); + } +} diff --git a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/serviceState/db/OqmDatabaseService.java b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/serviceState/db/OqmDatabaseService.java index 83e2245c99..05fc285d10 100644 --- a/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/serviceState/db/OqmDatabaseService.java +++ b/software/core/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/serviceState/db/OqmDatabaseService.java @@ -13,12 +13,13 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.bson.conversions.Bson; -import org.bson.types.ObjectId; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; +import tech.ebp.oqm.core.api.health.utils.HasReadinessCheck; import tech.ebp.oqm.core.api.model.collectionStats.CollectionStats; import tech.ebp.oqm.core.api.model.rest.search.OqmMongoDbSearch; import tech.ebp.oqm.core.api.service.mongo.TopLevelMongoService; +import tech.ebp.oqm.core.api.health.utils.HealthStatus; import java.util.ArrayList; import java.util.List; @@ -33,7 +34,7 @@ */ @Slf4j @ApplicationScoped -public class OqmDatabaseService extends TopLevelMongoService { +public class OqmDatabaseService extends TopLevelMongoService implements HasReadinessCheck { @Getter @Setter(AccessLevel.PRIVATE) @@ -46,6 +47,9 @@ public class OqmDatabaseService extends TopLevelMongoService getUnitsArgs() { return UnitUtils.UNIT_LIST.stream().map(Arguments::of); } + public static Stream getUnitsCompatMapArgs() { ArrayList args = new ArrayList<>(); - + for(Map.Entry, Set>> curUnitEntry : UnitUtils.UNIT_COMPATIBILITY_MAP.entrySet()){ for(Unit curCompatUnit : curUnitEntry.getValue()){ args.add(Arguments.of(curUnitEntry.getKey(), curCompatUnit)); } } - + return args.stream(); } + public static Stream getUnitsCompatMapListArgs() { ArrayList args = new ArrayList<>(); - + for(Map.Entry, Set>> curUnitEntry : UnitUtils.UNIT_COMPATIBILITY_MAP.entrySet()){ args.add(Arguments.of(curUnitEntry.getKey(), curUnitEntry.getValue())); } - + return args.stream(); } - + @ParameterizedTest @MethodSource("getUnitsArgs") public void testGetCompatibleUnitsJson(Unit unit) { @@ -71,7 +76,7 @@ public void testGetCompatibleUnitsJson(Unit unit) { .then() .statusCode(200); } - + @Test public void testGetDeriveTypes() { String deriveTypes = given() @@ -83,7 +88,7 @@ public void testGetDeriveTypes() { .asString(); log.info("Derive Types: {}", deriveTypes); } - + @Test public void testGetDimensions() { String deriveTypes = given() @@ -95,13 +100,13 @@ public void testGetDimensions() { .asString(); log.info("Dimensions: {}", deriveTypes); } - + @ParameterizedTest @MethodSource("getUnitsCompatMapArgs") public void testQuantityConvert(Unit quantityUnit, Unit toUnit) throws JsonProcessingException { Quantity quantity = Quantities.getQuantity(5, quantityUnit); ConvertRequest request = ConvertRequest.builder().quantity(quantity).newUnit(toUnit).build(); - + log.info("Calling convertEndpoint with: {}", request); ValidatableResponse response = given() .accept(MediaType.APPLICATION_JSON) @@ -109,25 +114,25 @@ public void testQuantityConvert(Unit quantityUnit, Unit toUnit) throws Jso .body(ObjectUtils.OBJECT_MAPPER.writeValueAsString(request)) .put("convert") .then(); - + log.info("Response status: {} Body: {}", response.extract().statusCode(), response.extract().body().asString()); - + response.statusCode(200); log.info("Resulting quantity: {}", ObjectUtils.OBJECT_MAPPER.readValue(response.extract().body().asString(), Quantity.class)); } - + @ParameterizedTest @MethodSource("getUnitsCompatMapListArgs") public void testQuantityListConvert(Unit quantityUnit, Set> toUnit) throws JsonProcessingException { List request = new ArrayList<>(); - + for(Unit curUnit : toUnit){ Quantity quantity = Quantities.getQuantity(5, quantityUnit); request.add( ConvertRequest.builder().quantity(quantity).newUnit(curUnit).build() ); } - + log.info("Calling convertEndpoint with: {}", request); ValidatableResponse response = given() .accept(MediaType.APPLICATION_JSON) @@ -135,12 +140,12 @@ public void testQuantityListConvert(Unit quantityUnit, Set> toUnit) t .body(ObjectUtils.OBJECT_MAPPER.writeValueAsString(request)) .put("convert") .then(); - + log.info("Response status: {} Body: {}", response.extract().statusCode(), response.extract().body().asString()); - + response.statusCode(200); log.info("Resulting quantity: {}", ObjectUtils.OBJECT_MAPPER.readValue(response.extract().body().asString(), new TypeReference>>() { })); } - + //TODO:: negative test for incompatible units -} \ No newline at end of file +} diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InventoryItemsStatsTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InventoryItemsStatsTest.java index 5d53292670..1a50a7e806 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InventoryItemsStatsTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/InventoryItemsStatsTest.java @@ -1,6 +1,7 @@ package tech.ebp.oqm.core.api.interfaces.endpoints.inventory.items; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.Test; import tech.ebp.oqm.core.api.model.object.interactingEntity.user.User; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.pricing.StoredPricing; import tech.ebp.oqm.core.api.model.object.storage.items.pricing.TotalPricing; @@ -37,7 +39,10 @@ import javax.money.Monetary; import javax.money.MonetaryAmount; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import static io.restassured.RestAssured.given; @@ -52,15 +57,15 @@ @Slf4j @QuarkusTest class InventoryItemsStatsTest extends RunningServerTest { - + @Inject InventoryItemTestObjectCreator testObjectCreator; @Inject StorageBlockTestObjectCreator testBlockCreator; - + @Inject InventoryItemService inventoryItemService; - + private StorageBlock newBlock(User testUser) throws JsonProcessingException { return OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -72,7 +77,7 @@ private StorageBlock newBlock(User testUser) throws JsonProcessingException { StorageBlock.class ); } - + private InventoryItem newItem(User testUser, StorageBlock... blocks) throws JsonProcessingException { return OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -80,9 +85,9 @@ private InventoryItem newItem(User testUser, StorageBlock... blocks) throws Json OBJECT_MAPPER.writeValueAsString( testObjectCreator.getTestObject() .setStorageType(StorageType.AMOUNT_LIST) - .setStorageBlocks(new LinkedHashSet<>() {{ + .setStorageBlocks(new LinkedList<>() {{ for (StorageBlock block : blocks) { - add(block.getId()); + add(StorageBlockSettings.builder().storageBlock(block.getId()).build()); } }}) ) @@ -94,42 +99,42 @@ private InventoryItem newItem(User testUser, StorageBlock... blocks) throws Json InventoryItem.class ); } - - + + @Test public void testItemStatsNoStored() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = this.newBlock(testUser); InventoryItem item = this.newItem(testUser, block); - + ItemStoredStats itemStats = item.getStats(); - + assertEquals(0, itemStats.getNumStored()); assertEquals(Quantities.getQuantity(0, OqmProvidedUnits.UNIT), itemStats.getTotal()); assertTrue(itemStats.getStorageBlockStats().containsKey(block.getId())); - - + + StoredInBlockStats blockStats = itemStats.getStorageBlockStats().get(block.getId()); - + assertFalse(blockStats.isHasStored()); } - + @Test public void testItemStatsWithStored() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = this.newBlock(testUser); InventoryItem item = this.newItem(testUser, block); - + //add stored - + AmountStored stored = AmountStored.builder() .amount(UnitUtils.Quantities.UNIT_ONE) .item(item.getId()) .state(StoredInBlock.builder().storageBlock(block.getId()).build()) .build(); - + AppliedTransaction transaction = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -147,7 +152,7 @@ public void testItemStatsWithStored() throws JsonProcessingException { .extract().body().asString(), AppliedTransaction.class ); - + //get back item for latest item = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -157,28 +162,28 @@ public void testItemStatsWithStored() throws JsonProcessingException { .extract().body().asString(), InventoryItem.class ); - - + + ItemStoredStats itemStats = item.getStats(); - + assertEquals(itemStats, transaction.getPostApplyResults().getStats()); - + assertEquals(1, itemStats.getNumStored()); assertEquals(Quantities.getQuantity(1, OqmProvidedUnits.UNIT), itemStats.getTotal()); assertTrue(itemStats.getStorageBlockStats().containsKey(block.getId())); - - + + StoredInBlockStats blockStats = itemStats.getStorageBlockStats().get(block.getId()); assertTrue(blockStats.isHasStored()); assertEquals(Quantities.getQuantity(1, OqmProvidedUnits.UNIT), blockStats.getTotal()); } - + @Test public void testPricingTotals() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = this.newBlock(testUser); - + InventoryItem item = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -186,8 +191,8 @@ public void testPricingTotals() throws JsonProcessingException { OBJECT_MAPPER.writeValueAsString( testObjectCreator.getTestObject() .setStorageType(StorageType.AMOUNT_LIST) - .setStorageBlocks(new LinkedHashSet<>() {{ - add(block.getId()); + .setStorageBlocks(new ArrayList<>() {{ + add(StorageBlockSettings.builder().storageBlock(block.getId()).build()); }} ) .setDefaultPrices(new LinkedHashSet<>() {{ @@ -207,7 +212,7 @@ public void testPricingTotals() throws JsonProcessingException { .price(Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create()) .unit(OqmProvidedUnits.UNIT) .build() - + ) .build()); }}) @@ -219,7 +224,7 @@ public void testPricingTotals() throws JsonProcessingException { .extract().body().asString(), InventoryItem.class ); - + AmountStored stored = AmountStored.builder() .amount(UnitUtils.Quantities.UNIT_ONE) .item(item.getId()) @@ -241,12 +246,12 @@ public void testPricingTotals() throws JsonProcessingException { .price(Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create()) .unit(OqmProvidedUnits.UNIT) .build() - + ) .build()); }}) .build(); - + AppliedTransaction transaction = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -264,7 +269,7 @@ public void testPricingTotals() throws JsonProcessingException { .extract().body().asString(), AppliedTransaction.class ); - + String finalItemStr = setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) .body( OBJECT_MAPPER.writeValueAsString( @@ -283,22 +288,22 @@ public void testPricingTotals() throws JsonProcessingException { finalItemStr, InventoryItem.class ); - + log.info("Item json: {}", finalItemStr); log.info("Item: {}", item); - + {//verify item stats LinkedHashSet prices = item.getStats().getPrices(); - + assertEquals(5, prices.size()); - + prices.stream().forEach((tp)->assertNotNull(tp.getTotalPrice())); - + assertEquals( Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(6).create(), prices.stream().map(TotalPricing::getTotalPrice).reduce(MonetaryAmount::add).get() ); - + assertEquals( Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create(), prices.stream().filter((p)->{ @@ -317,7 +322,7 @@ public void testPricingTotals() throws JsonProcessingException { return p.getLabel().equals("fromDefaultPerAmount"); }).findFirst().get().getTotalPrice() ); - + assertEquals( Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create(), prices.stream().filter((p)->{ @@ -330,25 +335,25 @@ public void testPricingTotals() throws JsonProcessingException { return p.getLabel().equals("fromStoredPerAmount"); }).findFirst().get().getTotalPrice() ); - + } {//verify stored stats LinkedHashSet prices = item.getStats().getStorageBlockStats().get(block.getId()).getPrices(); - + assertEquals(5, prices.size()); } - + //TODO:: verify pricing in item stats } - - + + @Test public void testItemStatsItemUpdateUnitNoStored() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = this.newBlock(testUser); InventoryItem item = this.newItem(testUser, block); - + item = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) .contentType(ContentType.JSON) @@ -360,60 +365,107 @@ public void testItemStatsItemUpdateUnitNoStored() throws JsonProcessingException .extract().body().asString(), InventoryItem.class ); - + ItemStoredStats itemStats = item.getStats(); - + assertEquals(0, itemStats.getNumStored()); assertEquals(Quantities.getQuantity(0, Units.MOLE), itemStats.getTotal()); assertTrue(itemStats.getStorageBlockStats().containsKey(block.getId())); - - + + StoredInBlockStats blockStats = itemStats.getStorageBlockStats().get(block.getId()); - + assertFalse(blockStats.isHasStored()); } - - + + @Test public void testItemStatsItemUpdateBlocksNoStored() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = this.newBlock(testUser); StorageBlock block2 = this.newBlock(testUser); InventoryItem item = this.newItem(testUser, block); - - item = OBJECT_MAPPER.readValue( - setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) - .contentType(ContentType.JSON) - .body("{\"storageBlocks\": [\"" + block2.getId() + "\", \"" + block.getId() + "\"]}") - .put( - "/api/v1/db/" + DEFAULT_TEST_DB_NAME + "/inventory/item/" + item.getId() - ) - .then().statusCode(200) - .extract().body().asString(), - InventoryItem.class - ); - + + + { + ObjectNode updates = OBJECT_MAPPER.createObjectNode(); + ArrayNode blockUpdates = updates.putArray("storageBlocks"); + blockUpdates.add(OBJECT_MAPPER.valueToTree(StorageBlockSettings.builder().storageBlock(block2.getId()).build())); + blockUpdates.add(OBJECT_MAPPER.valueToTree(StorageBlockSettings.builder().storageBlock(block.getId()).build())); + + item = OBJECT_MAPPER.readValue( + setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) + .contentType(ContentType.JSON) + .body(updates.toString()) + .put( + "/api/v1/db/" + DEFAULT_TEST_DB_NAME + "/inventory/item/" + item.getId() + ) + .then().statusCode(200) + .extract().body().asString(), + InventoryItem.class + ); + } + ItemStoredStats itemStats = item.getStats(); - + assertEquals(0, itemStats.getNumStored()); assertEquals(Quantities.getQuantity(0, OqmProvidedUnits.UNIT), itemStats.getTotal()); assertTrue(itemStats.getStorageBlockStats().containsKey(block.getId())); assertTrue(itemStats.getStorageBlockStats().containsKey(block2.getId())); - + StoredInBlockStats blockStats = itemStats.getStorageBlockStats().get(block.getId()); assertFalse(blockStats.isHasStored()); - + blockStats = itemStats.getStorageBlockStats().get(block2.getId()); assertFalse(blockStats.isHasStored()); } - + + @Test + public void testItemStatsItemUpdateBlockSettingsNoStored() throws JsonProcessingException { + User testUser = this.getTestUserService().getTestUser(); + + StorageBlock block = this.newBlock(testUser); + InventoryItem item = this.newItem(testUser, block); + + { + ObjectNode updates = OBJECT_MAPPER.createObjectNode(); + ArrayNode blockUpdates = OBJECT_MAPPER.valueToTree(item.getStorageBlocks()); + +// ((ObjectNode)blockUpdates.get(0)).put("notes", "fooBar"); + + updates.set("storageBlocks", blockUpdates); + + item = OBJECT_MAPPER.readValue( + setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) + .contentType(ContentType.JSON) + .body(updates.toString()) + .put( + "/api/v1/db/" + DEFAULT_TEST_DB_NAME + "/inventory/item/" + item.getId() + ) + .then().statusCode(200) + .extract().body().asString(), + InventoryItem.class + ); + } + + ItemStoredStats itemStats = item.getStats(); + + assertEquals(0, itemStats.getNumStored()); + assertEquals(Quantities.getQuantity(0, OqmProvidedUnits.UNIT), itemStats.getTotal()); + assertTrue(itemStats.getStorageBlockStats().containsKey(block.getId())); + + StoredInBlockStats blockStats = itemStats.getStorageBlockStats().get(block.getId()); + assertFalse(blockStats.isHasStored()); + } + //TODO:: test no stored, update expiry threshold, set //TODO:: test no stored, update expiry threshold, unset + //TODO:: test no stored, update low stock threshold, set //TODO:: test no stored, update low stock threshold, unset //TODO:: test no stored, update low stock threshold, default pricing - + //TODO:: test stored, update storage unit //TODO:: test stored, update storage blocks //TODO:: test stored, update expiry threshold, set @@ -421,7 +473,7 @@ public void testItemStatsItemUpdateBlocksNoStored() throws JsonProcessingExcepti //TODO:: test stored, update low stock threshold, set //TODO:: test stored, update low stock threshold, unset //TODO:: test stored, update low stock threshold, default pricing - + //TODO:: test stats, update stored; non-triggering //TODO:: test stats, update stored; pricing -} \ No newline at end of file +} diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/StoredInItemEndpointsTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/StoredInItemEndpointsTest.java index ec1835d2cf..3a8baff809 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/StoredInItemEndpointsTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/interfaces/endpoints/inventory/items/StoredInItemEndpointsTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import tech.ebp.oqm.core.api.model.object.interactingEntity.user.User; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.pricing.StoredPricing; import tech.ebp.oqm.core.api.model.object.storage.items.pricing.unit.PricePerUnit; @@ -33,7 +34,9 @@ import tech.ebp.oqm.core.api.testResources.testClasses.RunningServerTest; import javax.money.Monetary; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.LinkedList; import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -46,7 +49,7 @@ @QuarkusTest //@TestHTTPEndpoint(StoredEndpoints.class) public class StoredInItemEndpointsTest extends RunningServerTest { - + @Inject StorageBlockTestObjectCreator testBlockCreator; @Inject @@ -54,33 +57,33 @@ public class StoredInItemEndpointsTest extends RunningServerTest { @Inject ObjectMapper objectMapper; - + @Test public void testSearchEmptyDb() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + String json = setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) .body(objectMapper.writeValueAsString(testObjectCreator.getTestObject())) .contentType(ContentType.JSON) .post("/api/v1/db/"+DEFAULT_TEST_DB_NAME+"/inventory/item") .then().statusCode(200) .extract().body().asString(); - + String id = OBJECT_MAPPER.readValue(json, InventoryItem.class).getId().toHexString(); - + ValidatableResponse response = setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) .when() .get("/api/v1/db/"+DEFAULT_TEST_DB_NAME+"/inventory/item/"+id+"/stored") .then() .statusCode(200); - + log.info("Search result: {}", response.extract().asString()); } - + @Test public void testSearchSimple() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -91,8 +94,8 @@ public void testSearchSimple() throws JsonProcessingException { .extract().body().asString(), StorageBlock.class ); - - + + InventoryItem item = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -100,8 +103,8 @@ public void testSearchSimple() throws JsonProcessingException { OBJECT_MAPPER.writeValueAsString( testObjectCreator.getTestObject() .setStorageType(StorageType.AMOUNT_LIST) - .setStorageBlocks(new LinkedHashSet<>() {{ - add(block.getId()); + .setStorageBlocks(new ArrayList<>() {{ + add(StorageBlockSettings.builder().storageBlock(block.getId()).build()); }} ) .setDefaultPrices(new LinkedHashSet<>() {{ @@ -121,7 +124,7 @@ public void testSearchSimple() throws JsonProcessingException { .price(Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create()) .unit(OqmProvidedUnits.UNIT) .build() - + ) .build()); }}) @@ -133,7 +136,7 @@ public void testSearchSimple() throws JsonProcessingException { .extract().body().asString(), InventoryItem.class ); - + AmountStored stored = AmountStored.builder() .amount(UnitUtils.Quantities.UNIT_ONE) .item(item.getId()) @@ -155,12 +158,12 @@ public void testSearchSimple() throws JsonProcessingException { .price(Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create()) .unit(OqmProvidedUnits.UNIT) .build() - + ) .build()); }}) .build(); - + AppliedTransaction transaction = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -178,20 +181,20 @@ public void testSearchSimple() throws JsonProcessingException { .extract().body().asString(), AppliedTransaction.class ); - - + + ValidatableResponse response = setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) .when() .get("/api/v1/db/"+DEFAULT_TEST_DB_NAME+"/inventory/item/"+item.getId()+"/stored") .then() .statusCode(200); - + log.info("Search result: {}", response.extract().asString()); - + ObjectNode result = (ObjectNode) OBJECT_MAPPER.readTree(response.extract().asString()); - + assertEquals(1, result.get("numResults").asInt()); - + // SearchResult result = OBJECT_MAPPER.readValue(response.extract().asString(), new TypeReference<>() {}); // // assertEquals(1, result.getNumResults()); @@ -199,11 +202,11 @@ public void testSearchSimple() throws JsonProcessingException { // AmountStored storedResult = (AmountStored) result.getResults().getFirst(); // assertNotNull(storedResult.getCalculatedPrices()); } - + @Test public void testGetSimple() throws JsonProcessingException { User testUser = this.getTestUserService().getTestUser(); - + StorageBlock block = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -214,8 +217,8 @@ public void testGetSimple() throws JsonProcessingException { .extract().body().asString(), StorageBlock.class ); - - + + InventoryItem item = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -223,8 +226,8 @@ public void testGetSimple() throws JsonProcessingException { OBJECT_MAPPER.writeValueAsString( testObjectCreator.getTestObject() .setStorageType(StorageType.AMOUNT_LIST) - .setStorageBlocks(new LinkedHashSet<>() {{ - add(block.getId()); + .setStorageBlocks(new LinkedList<>() {{ + add(StorageBlockSettings.builder().storageBlock(block.getId()).build()); }} ) .setDefaultPrices(new LinkedHashSet<>() {{ @@ -244,7 +247,7 @@ public void testGetSimple() throws JsonProcessingException { .price(Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create()) .unit(OqmProvidedUnits.UNIT) .build() - + ) .build()); }}) @@ -256,7 +259,7 @@ public void testGetSimple() throws JsonProcessingException { .extract().body().asString(), InventoryItem.class ); - + AmountStored stored = AmountStored.builder() .amount(UnitUtils.Quantities.UNIT_ONE) .item(item.getId()) @@ -278,12 +281,12 @@ public void testGetSimple() throws JsonProcessingException { .price(Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create()) .unit(OqmProvidedUnits.UNIT) .build() - + ) .build()); }}) .build(); - + AppliedTransaction transaction = OBJECT_MAPPER.readValue( setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) @@ -301,19 +304,19 @@ public void testGetSimple() throws JsonProcessingException { .extract().body().asString(), AppliedTransaction.class ); - - + + ValidatableResponse response = setupJwtCall(given(), testUser.getAttributes().get(TestUserService.TEST_JWT_ATT_KEY)) .when() .get("/api/v1/db/"+DEFAULT_TEST_DB_NAME+"/inventory/item/"+item.getId()+"/stored/" + transaction.getAffectedStored().stream().findFirst().get().toHexString()) .then() .statusCode(200); - + log.info("result: {}", response.extract().asString()); - + AmountStored result = OBJECT_MAPPER.readValue(response.extract().asString(), AmountStored.class); //TODO:: assertions // assertNotNull(result.getCalculatedPrices()); } - + } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StorageBlockServiceTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StorageBlockServiceTest.java index feecee9df1..35d49bbb53 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StorageBlockServiceTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StorageBlockServiceTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import tech.ebp.oqm.core.api.exception.db.DbDeleteRelationalException; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.testResources.data.StorageBlockTestObjectCreator; import tech.ebp.oqm.core.api.testResources.testClasses.MongoHistoriedServiceTest; @@ -25,12 +26,12 @@ @Slf4j @QuarkusTest class StorageBlockServiceTest extends MongoHistoriedServiceTest { - + StorageBlockService storageBlockService; InventoryItemService inventoryItemService; - + StorageBlockTestObjectCreator storageBlockTestObjectCreator; - + @Inject StorageBlockServiceTest( StorageBlockService storageBlockService, @@ -39,59 +40,59 @@ class StorageBlockServiceTest extends MongoHistoriedServiceTest> expectedRefs = new HashMap<>(); - + this.storageBlockService.add(DEFAULT_TEST_DB_NAME, storageBlock, testUser); {//setup referencing data //parent @@ -99,24 +100,24 @@ public void testDeleteWithRelational(){ subBlock.setParent(storageBlock.getId()); ObjectId subBlockId = this.storageBlockService.add(DEFAULT_TEST_DB_NAME, subBlock, testUser).getId(); expectedRefs.put(this.storageBlockService.getClazz().getSimpleName(), new TreeSet<>(List.of(subBlockId))); - + //Inventory item, basic this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, InventoryItem.builder().name(FAKER.name().name()).storageType(StorageType.BULK).build(), testUser); InventoryItem sai = InventoryItem.builder().name(FAKER.name().name()).storageType(StorageType.BULK).storageBlocks( - new LinkedHashSet<>(List.of(storageBlock.getId())) + new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(storageBlock.getId()).build())) ).build(); ObjectId itemId = this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, sai, testUser).getId(); expectedRefs.put(this.inventoryItemService.getClazz().getSimpleName(), new TreeSet<>(List.of(itemId))); } - + DbDeleteRelationalException exception = assertThrows( DbDeleteRelationalException.class, ()->this.storageBlockService.remove(DEFAULT_TEST_DB_NAME, storageBlock.getId(), testUser) ); - + log.info("Referenced objects: {}", exception.getObjectsReferencing()); assertEquals(expectedRefs, exception.getObjectsReferencing()); } -} \ No newline at end of file +} diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StoredServiceTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StoredServiceTest.java index c3774c5bb9..899ba06af3 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StoredServiceTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/StoredServiceTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import tech.ebp.oqm.core.api.model.object.interactingEntity.user.User; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -22,6 +23,7 @@ import tech.ebp.oqm.core.api.testResources.testClasses.MongoHistoriedServiceTest; import tech.units.indriya.quantity.Quantities; +import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -31,25 +33,25 @@ @Slf4j @QuarkusTest class StoredServiceTest extends MongoHistoriedServiceTest { - + @Inject InventoryItemService inventoryItemService; @Inject StorageBlockService storageBlockService; - + @Inject InventoryItemTestObjectCreator itemTestObjectCreator; @Inject StoredTestObjectCreator storedTestObjectCreator; - + @Inject StoredService storedService; - + @Override protected Stored getTestObject() { return storedTestObjectCreator.getTestObject(); } - + // @Test // public void injectTest() { // assertNotNull(inventoryItemService); @@ -84,7 +86,7 @@ protected Stored getTestObject() { // public void removeAllTest() { // this.defaultRemoveAllTest(this.inventoryItemService); // } - + @Test public void addTest() { User user = this.getTestUserService().getTestUser(); @@ -94,21 +96,21 @@ public void addTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + Stored stored = this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(); - + ObjectId storedId = this.storedService.add( DEFAULT_TEST_DB_NAME, stored, user ).getId(); - + assertNotNull(storedId); assertEquals(stored, this.storedService.get(DEFAULT_TEST_DB_NAME, storedId)); } - + @Test public void addInvalidItemTest() { User user = this.getTestUserService().getTestUser(); @@ -118,12 +120,12 @@ public void addInvalidItemTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + Stored stored = this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(); stored.setItem(new ObjectId()); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -132,7 +134,7 @@ public void addInvalidItemTest() { ) ); } - + @Test public void addInvalidNonExistentStorageBlockTest() { User user = this.getTestUserService().getTestUser(); @@ -142,12 +144,12 @@ public void addInvalidNonExistentStorageBlockTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + Stored stored = this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(); stored.setState(StoredInBlock.builder().storageBlock(ObjectId.get()).build()); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -156,7 +158,7 @@ public void addInvalidNonExistentStorageBlockTest() { ) ); } - + @Test public void addInvalidStorageBlockNotInItemTest() { User user = this.getTestUserService().getTestUser(); @@ -166,9 +168,9 @@ public void addInvalidStorageBlockNotInItemTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + Stored stored = this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(); stored.setState( StoredInBlock.builder().storageBlock( @@ -179,7 +181,7 @@ public void addInvalidStorageBlockNotInItemTest() { ).getId() ).build() ); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -188,7 +190,7 @@ public void addInvalidStorageBlockNotInItemTest() { ) ); } - + @Test public void addInvalidTypeTest() { User user = this.getTestUserService().getTestUser(); @@ -198,11 +200,11 @@ public void addInvalidTypeTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); ObjectId itemId = this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user).getId(); - + Stored stored = UniqueStored.builder().item(itemId).state(StoredInBlock.builder().storageBlock(blockId).build()).build(); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -211,7 +213,7 @@ public void addInvalidTypeTest() { ) ); } - + @Test public void addInvalidUnitTest() { User user = this.getTestUserService().getTestUser(); @@ -221,12 +223,12 @@ public void addInvalidUnitTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + Stored stored = this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(); ((AmountStored) stored).setAmount(Quantities.getQuantity(0, OqmProvidedUnits.WATT_HOURS)); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -235,7 +237,7 @@ public void addInvalidUnitTest() { ) ); } - + @Test public void addInvalidSecondBulkTest() { User user = this.getTestUserService().getTestUser(); @@ -245,11 +247,11 @@ public void addInvalidSecondBulkTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + this.storedService.add(DEFAULT_TEST_DB_NAME, this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(), user); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -258,7 +260,7 @@ public void addInvalidSecondBulkTest() { ) ); } - + @Test public void addInvalidSecondUniqueSingleSameBlockTest() { User user = this.getTestUserService().getTestUser(); @@ -268,11 +270,11 @@ public void addInvalidSecondUniqueSingleSameBlockTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.UNIQUE_SINGLE).setStorageBlocks(new LinkedHashSet<>(List.of(blockId))); + item.setStorageType(StorageType.UNIQUE_SINGLE).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockId).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + this.storedService.add(DEFAULT_TEST_DB_NAME, this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(), user); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -281,7 +283,7 @@ public void addInvalidSecondUniqueSingleSameBlockTest() { ) ); } - + @Test public void addInvalidSecondUniqueSingleDifferentBlockTest() { User user = this.getTestUserService().getTestUser(); @@ -296,11 +298,11 @@ public void addInvalidSecondUniqueSingleDifferentBlockTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.UNIQUE_SINGLE).setStorageBlocks(new LinkedHashSet<>(List.of(blockIdOne))); + item.setStorageType(StorageType.UNIQUE_SINGLE).setStorageBlocks(new ArrayList<>(List.of(StorageBlockSettings.builder().storageBlock(blockIdOne).build()))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + this.storedService.add(DEFAULT_TEST_DB_NAME, this.storedTestObjectCreator.setItem(item).setStorageBlock(blockIdOne).getTestObject(), user); - + Exception exception = assertThrows( ValidationException.class, ()->this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -309,8 +311,8 @@ public void addInvalidSecondUniqueSingleDifferentBlockTest() { ) ); } - - + + @Test public void searchInStorageBlockTest() { User user = this.getTestUserService().getTestUser(); @@ -325,28 +327,31 @@ public void searchInStorageBlockTest() { user ).getId(); InventoryItem item = this.itemTestObjectCreator.getTestObject(); - item.setStorageType(StorageType.BULK).setStorageBlocks(new LinkedHashSet<>(List.of(blockId, otherBlockId))); + item.setStorageType(StorageType.BULK).setStorageBlocks(new ArrayList<>(List.of( + StorageBlockSettings.builder().storageBlock(blockId).build(), + StorageBlockSettings.builder().storageBlock(otherBlockId).build() + ))); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, user); - + Stored stored = this.storedTestObjectCreator.setItem(item).setStorageBlock(blockId).getTestObject(); Stored otherStored = this.storedTestObjectCreator.setItem(item).setStorageBlock(otherBlockId).getTestObject(); - + this.storedService.add(DEFAULT_TEST_DB_NAME, stored, user); this.storedService.add(DEFAULT_TEST_DB_NAME, otherStored, user); - + SearchResult result = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInStorageBlocks(List.of(blockId))); - + assertEquals(1, result.getNumResults()); assertEquals(stored, result.getResults().get(0)); } - + //TODO:: update //TODO:: invalid update; amount, item, storage block //TODO:: delete - + //TODO:: tests with images, file? - - + + //TODO:: rework // @Test // public void testAddSimpleAmountItem(){ @@ -443,8 +448,8 @@ public void searchInStorageBlockTest() { // // assertEquals(newName, item2.getName()); // } - - + + //TODO:: rework // @Test // public void testStoreItemWithNulledStorageBlockKey(){ @@ -455,11 +460,11 @@ public void searchInStorageBlockTest() { // SimpleAmountItem returned = (SimpleAmountItem) this.inventoryItemService.get(DEFAULT_TEST_DB_NAME, id); // assertEquals(item, returned); // } - + //TODO:: test expiry related - + // @Test // public void listTest(){ // // } -} \ No newline at end of file +} diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddAmountAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddAmountAppliedTransactionTest.java index e771366c07..714342f7c7 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddAmountAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddAmountAppliedTransactionTest.java @@ -13,6 +13,7 @@ import tech.ebp.oqm.core.api.model.object.history.events.UpdateEvent; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -39,52 +40,52 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class AddAmountAppliedTransactionTest extends AppliedTransactionServiceTest { - + @Test public void applyAddAmountSuccessBulkNotInBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - - + + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + try { this.clearQueues(); } catch(UnknownTopicOrPartitionException e) { log.warn("Failed to clear queues", e); } - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - - + + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + //TODO:: this, #1080 // List messages = this.assertMessages( // EventType.CREATE, @@ -92,251 +93,251 @@ public void applyAddAmountSuccessBulkNotInBlock() throws Exception { // ); //TODO:: verify messages } - + @Test public void applyAddAmountSuccessBulkAlreadyInBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + AmountStored originalStored = AmountStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, originalStored, entity); - - + + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(10, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(originalStored.getId(), stored.getId()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(10, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applyAddAmountSuccessAmtListNewEntry() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applyAddAmountSuccessAmtListExistingStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + AmountStored originalStored = AmountStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, originalStored, entity); - - + + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .toStored(originalStored.getId()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(10, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(originalStored.getId(), stored.getId()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(10, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applyAddAmountFailUniqueMulti() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot add an amount to a unique item.", e.getMessage()); } - + @Test public void applyAddAmountFailUniqueSingle() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot add an amount to a unique item.", e.getMessage()); } - + @Test public void applyAddAmountFailBulkBlockNotInInventory() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + ObjectId badBlockId = new ObjectId(); ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .toBlock(badBlockId) .build(); - + ValidationException e = assertThrows(ValidationException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Storage block " + badBlockId + " not used to hold this item (" + item.getId().toHexString() + ").", e.getMessage()); } - + @Test public void applyAddAmountFailBulkStoredNotInBlock() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + ObjectId badStoredId = new ObjectId(); ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .toStored(badStoredId) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Stored given does not match stored found in block.", e.getMessage()); } - + @Test public void applyAddAmountFailAmtListBadStored() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + ObjectId badStoredId = new ObjectId(); ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .toStored(badStoredId) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Could not find Stored with id " + badStoredId, e.getMessage()); } - + @Test public void applyAddAmountFailAmtListBadStoredNotInBlock() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - - ObjectId origBlock = item.getStorageBlocks().getFirst(); + + ObjectId origBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId otherBlockId = this.storageBlockService.add(DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity).getId(); - - item.getStorageBlocks().add(otherBlockId); + + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(otherBlockId).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored originalStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(origBlock).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, originalStored, entity); - + ItemStoredTransaction preApplyTransaction = AddAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .toStored(originalStored.getId()) .toBlock(otherBlockId) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Stored given does not exist in block.", e.getMessage()); } - + //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddWholeAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddWholeAppliedTransactionTest.java index 908cb01be1..c1e9c5aa11 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddWholeAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AddWholeAppliedTransactionTest.java @@ -42,183 +42,183 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class AddWholeAppliedTransactionTest extends AppliedTransactionServiceTest { - + @Test public void applyAddWholeSuccessAmtList() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( AmountStored.builder() .item(item.getId()) .amount(Quantities.getQuantity(5, item.getUnit())) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); CreateEvent event = (CreateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applyAddWholeSuccessUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); UniqueStored storedFromSearch = (UniqueStored) storedSearchResult.getResults().getFirst(); - + UniqueStored stored = (UniqueStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); CreateEvent event = (CreateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applyAddWholeSuccessUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); UniqueStored storedFromSearch = (UniqueStored) storedSearchResult.getResults().getFirst(); - + UniqueStored stored = (UniqueStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); CreateEvent event = (CreateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applyAddWholeFailBulk() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( AmountStored.builder() .item(item.getId()) .amount(Quantities.getQuantity(5, item.getUnit())) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot add whole item to a bulk storage typed item.", e.getMessage()); } - + @Test public void applyAddWholeFailAnotherUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + ValidationException e = assertThrows(ValidationException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot store more than one globally unique stored item.", e.getMessage()); } - + @Test public void applyAddWholeFailMismatchedBlock() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() @@ -226,33 +226,33 @@ public void applyAddWholeFailMismatchedBlock() { .state(StoredInBlock.builder().storageBlock(ObjectId.get()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Block given does not match block marked in stored.", e.getMessage()); - + ItemStoredTransaction preApplyTransaction2 = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) .toBlock(new ObjectId()) .build(); - + e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction2, entity)); assertEquals("To Block given does not match block marked in stored.", e.getMessage()); } - + @Test public void applyAddWholeFailBlockNotInItem() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() @@ -260,35 +260,35 @@ public void applyAddWholeFailBlockNotInItem() { .state(StoredInBlock.builder().storageBlock(ObjectId.get()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Block given does not match block marked in stored.", e.getMessage()); } - + @Test public void applyAddWholeFailItemMismatched() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = AddWholeTransaction.builder() .toAdd( UniqueStored.builder() .item(new ObjectId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build() ) - .toBlock(item.getStorageBlocks().getFirst()) + .toBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Stored given is not associated with item.", e.getMessage()); } - + //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AppliedTransactionServiceTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AppliedTransactionServiceTest.java index 0fcf454147..650af804f8 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AppliedTransactionServiceTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/AppliedTransactionServiceTest.java @@ -32,6 +32,7 @@ import tech.ebp.oqm.core.api.model.object.storage.checkout.checkinDetails.checkedInBy.CheckedInByOqmEntity; import tech.ebp.oqm.core.api.model.object.storage.checkout.checkoutFor.CheckoutForOqmEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -80,32 +81,32 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) abstract class AppliedTransactionServiceTest extends MongoObjectServiceTest implements KafkaTest { - + @Inject AppliedTransactionService appliedTransactionService; - + @Inject StorageBlockService storageBlockService; - + @Inject StorageBlockTestObjectCreator storageBlockTestObjectCreator; - + @Inject InventoryItemService inventoryItemService; - + @Inject InventoryItemTestObjectCreator itemTestObjectCreator; - + @Inject StoredService storedService; - + @Inject ItemCheckoutService checkoutService; - + @Getter @InjectKafkaCompanion KafkaCompanion kafkaCompanion; - + //TODO:: these default tests @Override protected AppliedTransaction getTestObject() { @@ -146,17 +147,17 @@ protected AppliedTransaction getTestObject() { // public void removeAllTest() { // this.defaultRemoveAllTest(this.appliedTransactionService); // } - - + + protected InventoryItem setupItem(StorageType storageType, InteractingEntity entity) { StorageBlock storageBlock = storageBlockTestObjectCreator.getTestObject(); this.storageBlockService.add(DEFAULT_TEST_DB_NAME, storageBlock, entity); InventoryItem item = this.itemTestObjectCreator.getTestObject().setStorageType(storageType); - item.getStorageBlocks().add(storageBlock.getId()); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(storageBlock.getId()).build()); this.inventoryItemService.add(DEFAULT_TEST_DB_NAME, item, entity); return item; } - + protected void clearQueues() { this.kafkaCompanion .topics() @@ -164,7 +165,7 @@ protected void clearQueues() { // .clear(HistoryEventNotificationService.ALL_EVENT_TOPIC) ; } - + //TODO:: this with all following tests protected List assertMessages( EventType... expectedEvents @@ -195,17 +196,17 @@ protected List assertMessages( // awaiting = false; // } // }while(awaiting); - - + + try { - + ConsumerTask messagesFromAll = this.kafkaCompanion.consumeStrings().fromTopics( HistoryEventNotificationService.ALL_EVENT_TOPIC , expectedEvents.length ); messagesFromAll.awaitCompletion(); assertEquals(expectedEvents.length, messagesFromAll.count()); - + List eventWrappers = messagesFromAll.stream() .map(record->{ try { @@ -216,30 +217,30 @@ protected List assertMessages( }) .collect(Collectors.toList()); log.info("Found messages for transaction: {}", eventWrappers); - + assertEquals(expectedEvents.length, eventWrappers.size()); - + for (int i = 0; i < expectedEvents.length; i++) { EventNotificationWrapper curWrapper = eventWrappers.get(i); EventType expectedType = expectedEvents[i]; - + assertEquals(expectedType, curWrapper.getEvent().getType(), "Unexpected type of transaction at index " + i); } - + return eventWrappers; } catch(UnknownTopicOrPartitionException e) { log.warn("Failed to clear queues", e); throw e; } } - + // //TODO:: When we have this implemented // // //TODO:: When we have this implemented // - + // //TODO:: fix these, inconsistent and flaky // @Test @@ -285,11 +286,11 @@ protected List assertMessages( // List messages = this.assertMessages(EventType.UPDATE, EventType.UPDATE, EventType.UPDATE, EventType.ITEM_LOW_STOCK); // //TODO:: assert events, notification state // } - - + + //TODO:: low stock (stored) //TODO:: low stock (item) //TODO:: expiry warn alert //TODO:: expired alert // -} \ No newline at end of file +} diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinFullAmountAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinFullAmountAppliedTransactionTest.java index ea0a6e3402..56b14390ca 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinFullAmountAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinFullAmountAppliedTransactionTest.java @@ -19,6 +19,7 @@ import tech.ebp.oqm.core.api.model.object.storage.checkout.checkinDetails.checkedInBy.CheckedInByOqmEntity; import tech.ebp.oqm.core.api.model.object.storage.checkout.checkoutFor.CheckoutForOqmEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -53,15 +54,15 @@ public class CheckinFullAmountAppliedTransactionTest extends AppliedTransactionS public void applyCheckinFullAmountSuccessBulkToBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutAmountTransaction.builder() @@ -77,7 +78,7 @@ public void applyCheckinFullAmountSuccessBulkToBlock() throws Exception { .getFirst() .getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -86,61 +87,61 @@ public void applyCheckinFullAmountSuccessBulkToBlock() throws Exception { .toBlock(blockId) .details(details) .build(); - + appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + @Test public void applyCheckinFullAmountSuccessBulkToBlockWithoutStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId origBlockId = item.getStorageBlocks().getFirst(); + ObjectId origBlockId = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId newBlockId = this.storageBlockService.add(DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity).getId(); - item.getStorageBlocks().add(newBlockId); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(newBlockId).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(origBlockId).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutAmountTransaction.builder() @@ -154,7 +155,7 @@ public void applyCheckinFullAmountSuccessBulkToBlockWithoutStored() throws Excep checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -163,58 +164,58 @@ public void applyCheckinFullAmountSuccessBulkToBlockWithoutStored() throws Excep .toBlock(newBlockId) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(4, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + @Test public void applyCheckinFullAmountSuccessBulkToStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutAmountTransaction.builder() @@ -228,7 +229,7 @@ public void applyCheckinFullAmountSuccessBulkToStored() throws Exception { checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -237,58 +238,58 @@ public void applyCheckinFullAmountSuccessBulkToStored() throws Exception { .toStored(initialStored.getId()) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + @Test public void applyCheckinFullAmountSuccessAmtListToStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutAmountTransaction.builder() @@ -302,7 +303,7 @@ public void applyCheckinFullAmountSuccessAmtListToStored() throws Exception { checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); ItemStoredTransaction preApplyTransaction = CheckinFullTransaction.builder() @@ -310,58 +311,58 @@ public void applyCheckinFullAmountSuccessAmtListToStored() throws Exception { .toStored(initialStored.getId()) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + @Test public void applyCheckinFullAmountSuccessAmtListToBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutAmountTransaction.builder() @@ -375,7 +376,7 @@ public void applyCheckinFullAmountSuccessAmtListToBlock() throws Exception { checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -384,44 +385,44 @@ public void applyCheckinFullAmountSuccessAmtListToBlock() throws Exception { .toBlock(blockId) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(4, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + //TODO:: more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinWholeAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinWholeAppliedTransactionTest.java index d3b6c46d40..e37ef5d5f5 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinWholeAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckinWholeAppliedTransactionTest.java @@ -48,21 +48,21 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class CheckinWholeAppliedTransactionTest extends AppliedTransactionServiceTest { - - + + @Test public void applyCheckinFullWholeSuccessBulk() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutWholeTransaction.builder() @@ -75,7 +75,7 @@ public void applyCheckinFullWholeSuccessBulk() throws Exception { checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -84,57 +84,57 @@ public void applyCheckinFullWholeSuccessBulk() throws Exception { .toBlock(blockId) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); ReCreateEvent event = (ReCreateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + @Test public void applyCheckinFullWholeSuccessUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + UniqueStored initialStored = UniqueStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutWholeTransaction.builder() @@ -147,7 +147,7 @@ public void applyCheckinFullWholeSuccessUniqueMulti() throws Exception { checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -156,56 +156,56 @@ public void applyCheckinFullWholeSuccessUniqueMulti() throws Exception { .toBlock(blockId) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); UniqueStored storedFromSearch = (UniqueStored) storedSearchResult.getResults().getFirst(); - + UniqueStored stored = (UniqueStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); ReCreateEvent event = (ReCreateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - + @Test public void applyCheckinFullWholeSuccessUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + UniqueStored initialStored = UniqueStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, initialStored, entity); - + ObjectId checkoutTransactionId = this.appliedTransactionService.apply( DEFAULT_TEST_DB_NAME, null, item, CheckoutWholeTransaction.builder() @@ -218,7 +218,7 @@ public void applyCheckinFullWholeSuccessUniqueSingle() throws Exception { checkoutId = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setCheckOutTransaction(checkoutTransactionId)).getResults().getFirst().getId(); // AppliedTransaction checkoutTransactionId = this.appliedTransactionService.get(DEFAULT_TEST_DB_NAME, appliedTransactionId); - + ReturnFullCheckinDetails details = ReturnFullCheckinDetails.builder().notes(FAKER.lorem().paragraph()).checkedInBy(CheckedInByOqmEntity.builder().entity(entity.getId()).build()).build(); @@ -227,45 +227,45 @@ public void applyCheckinFullWholeSuccessUniqueSingle() throws Exception { .toBlock(blockId) .details(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); UniqueStored storedFromSearch = (UniqueStored) storedSearchResult.getResults().getFirst(); - + UniqueStored stored = (UniqueStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); ReCreateEvent event = (ReCreateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - - + + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setStillCheckedOut(false).setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckInTransaction()); assertEquals(details, resultingCheckout.getCheckInDetails()); } - - + + //TODO:: fail - amount list - amount - to stored (not existing) //TODO:: Fail - amount - No 'to' given //TODO:: Fail - amount - 'to block' given for list diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutAmountAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutAmountAppliedTransactionTest.java index 0310262e90..a7b31fc11c 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutAmountAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutAmountAppliedTransactionTest.java @@ -46,13 +46,13 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class CheckoutAmountAppliedTransactionTest extends AppliedTransactionServiceTest { - + @Test public void applyCheckoutAmountSuccessBulkFromBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -62,7 +62,7 @@ public void applyCheckoutAmountSuccessBulkFromBlock() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -71,50 +71,50 @@ public void applyCheckoutAmountSuccessBulkFromBlock() throws Exception { .fromBlock(blockId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(1, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(initialStoredId, resultingCheckout.getFromStored()); assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(Quantities.getQuantity(5, item.getUnit()), resultingCheckout.getCheckedOut()); } - + @Test public void applyCheckoutAmountSuccessBulkFromStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -124,7 +124,7 @@ public void applyCheckoutAmountSuccessBulkFromStored() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -133,50 +133,50 @@ public void applyCheckoutAmountSuccessBulkFromStored() throws Exception { .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(1, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(initialStoredId, resultingCheckout.getFromStored()); assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(Quantities.getQuantity(5, item.getUnit()), resultingCheckout.getCheckedOut()); } - + @Test public void applyCheckoutAmountSuccessAmtList() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -186,7 +186,7 @@ public void applyCheckoutAmountSuccessAmtList() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -195,50 +195,50 @@ public void applyCheckoutAmountSuccessAmtList() throws Exception { .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(1, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemAmountCheckout resultingCheckout = (ItemAmountCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(initialStoredId, resultingCheckout.getFromStored()); assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(Quantities.getQuantity(5, item.getUnit()), resultingCheckout.getCheckedOut()); } - + @Test public void applyCheckoutAmountFailBulkFromNothing() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -248,7 +248,7 @@ public void applyCheckoutAmountFailBulkFromNothing() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -256,19 +256,19 @@ public void applyCheckoutAmountFailBulkFromNothing() throws Exception { .amount(Quantities.getQuantity(5, item.getUnit())) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("No stored or block given to checkout from.", e.getMessage()); } - + @Test public void applyCheckoutAmountFailAmtListFromNothing() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -278,7 +278,7 @@ public void applyCheckoutAmountFailAmtListFromNothing() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -286,19 +286,19 @@ public void applyCheckoutAmountFailAmtListFromNothing() throws Exception { .amount(Quantities.getQuantity(5, item.getUnit())) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("No stored given to checkout from.", e.getMessage()); } - + @Test public void applyCheckoutAmountFailAmtListFromNoStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -308,7 +308,7 @@ public void applyCheckoutAmountFailAmtListFromNoStored() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -317,19 +317,19 @@ public void applyCheckoutAmountFailAmtListFromNoStored() throws Exception { .fromBlock(blockId) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("No stored given to checkout from.", e.getMessage()); } - + @Test public void applyCheckoutAmountFailAmtListBadBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -339,7 +339,7 @@ public void applyCheckoutAmountFailAmtListBadBlock() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -349,19 +349,19 @@ public void applyCheckoutAmountFailAmtListBadBlock() throws Exception { .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("From Storage block given mismatched stored's block.", e.getMessage()); } - + @Test public void applyCheckoutAmountFailBulkNotEnoughToCheckout() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -371,7 +371,7 @@ public void applyCheckoutAmountFailBulkNotEnoughToCheckout() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -381,19 +381,19 @@ public void applyCheckoutAmountFailBulkNotEnoughToCheckout() throws Exception { .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Resulting amount less than zero. (subtracting 7 units from 6 units resulting in -1 units)", e.getMessage()); } - + @Test public void applyCheckoutAmountFailAmtListNotEnoughToCheckout() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() @@ -403,7 +403,7 @@ public void applyCheckoutAmountFailAmtListNotEnoughToCheckout() throws Exception .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -413,19 +413,19 @@ public void applyCheckoutAmountFailAmtListNotEnoughToCheckout() throws Exception .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Resulting amount less than zero. (subtracting 7 units from 6 units resulting in -1 units)", e.getMessage()); } - + @Test public void applyCheckoutAmountFailUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, UniqueStored.builder() @@ -434,7 +434,7 @@ public void applyCheckoutAmountFailUniqueSingle() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -444,19 +444,19 @@ public void applyCheckoutAmountFailUniqueSingle() throws Exception { .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot checkout an amount from a unique type.", e.getMessage()); } - + @Test public void applyCheckoutAmountFailUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, UniqueStored.builder() @@ -465,7 +465,7 @@ public void applyCheckoutAmountFailUniqueMulti() throws Exception { .build(), entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -475,13 +475,13 @@ public void applyCheckoutAmountFailUniqueMulti() throws Exception { .fromStored(initialStoredId) .checkoutDetails(details) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot checkout an amount from a unique type.", e.getMessage()); } - + //TODO:: fail - mismatched item/stored/block //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutWholeAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutWholeAppliedTransactionTest.java index e6f7d1aed7..af183ea200 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutWholeAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/CheckoutWholeAppliedTransactionTest.java @@ -45,13 +45,13 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class CheckoutWholeAppliedTransactionTest extends AppliedTransactionServiceTest { - + @Test public void applyCheckoutWholeSuccessBulk() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) @@ -62,7 +62,7 @@ public void applyCheckoutWholeSuccessBulk() throws Exception { initialStored, entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -70,44 +70,44 @@ public void applyCheckoutWholeSuccessBulk() throws Exception { .toCheckout(initialStoredId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(0, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 0); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(initialStoredId)); assertFalse(storedHistory.isEmpty()); DeleteEvent event = (DeleteEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(initialStored, resultingCheckout.getCheckedOut()); } - + @Test public void applyCheckoutWholeSuccessAmtList() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + AmountStored initialStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) @@ -118,7 +118,7 @@ public void applyCheckoutWholeSuccessAmtList() throws Exception { initialStored, entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -126,44 +126,44 @@ public void applyCheckoutWholeSuccessAmtList() throws Exception { .toCheckout(initialStoredId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(0, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 0); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(initialStoredId)); assertFalse(storedHistory.isEmpty()); DeleteEvent event = (DeleteEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(initialStored, resultingCheckout.getCheckedOut()); } - + @Test public void applyCheckoutWholeSuccessUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + UniqueStored initialStored = UniqueStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) @@ -173,7 +173,7 @@ public void applyCheckoutWholeSuccessUniqueMulti() throws Exception { initialStored, entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -181,44 +181,44 @@ public void applyCheckoutWholeSuccessUniqueMulti() throws Exception { .toCheckout(initialStoredId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(0, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 0); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(initialStoredId)); assertFalse(storedHistory.isEmpty()); DeleteEvent event = (DeleteEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(initialStored, resultingCheckout.getCheckedOut()); } - + @Test public void applyCheckoutWholeSuccessUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - ObjectId blockId = item.getStorageBlocks().getFirst(); - + ObjectId blockId = item.getStorageBlocks().getFirst().getStorageBlock(); + UniqueStored initialStored = UniqueStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(blockId).build()) @@ -228,7 +228,7 @@ public void applyCheckoutWholeSuccessUniqueSingle() throws Exception { initialStored, entity ).getId(); - + CheckoutDetails details = CheckoutDetails.builder() .checkedOutFor(CheckoutForOqmEntity.builder().entity(entity.getId()).build()) .build(); @@ -236,38 +236,38 @@ public void applyCheckoutWholeSuccessUniqueSingle() throws Exception { .toCheckout(initialStoredId) .checkoutDetails(details) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(0, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 0); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(initialStoredId)); assertFalse(storedHistory.isEmpty()); DeleteEvent event = (DeleteEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); - + SearchResult checkoutSearch = this.checkoutService.search(DEFAULT_TEST_DB_NAME, new ItemCheckoutSearch().setItemCheckedOut(item.getId())); assertEquals(checkoutSearch.getNumResults(), 1); ItemWholeCheckout resultingCheckout = (ItemWholeCheckout) checkoutSearch.getResults().getFirst(); - + assertEquals(details, resultingCheckout.getCheckoutDetails()); assertEquals(appliedTransaction.getId(), resultingCheckout.getCheckOutTransaction()); assertEquals(initialStored, resultingCheckout.getCheckedOut()); } - + //TODO:: fail - mismatched item/stored/block //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SetAmountAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SetAmountAppliedTransactionTest.java index e95343ea64..7354956213 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SetAmountAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SetAmountAppliedTransactionTest.java @@ -17,6 +17,7 @@ import tech.ebp.oqm.core.api.model.object.storage.checkout.ItemWholeCheckout; import tech.ebp.oqm.core.api.model.object.storage.checkout.checkoutFor.CheckoutForOqmEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -50,263 +51,263 @@ public class SetAmountAppliedTransactionTest extends AppliedTransactionServiceTe public void applySetAmountSuccessBulkNotInBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(5, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - - + + @Test public void applySetAmountSuccessBulkAlreadyInBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + AmountStored originalStored = AmountStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, originalStored, entity); - - + + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(6, item.getUnit())) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(6, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(originalStored.getId(), stored.getId()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(6, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - - + + @Test public void applySetAmountSuccessAmtListExistingStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + AmountStored originalStored = AmountStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, originalStored, entity); - - + + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(6, item.getUnit())) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .stored(originalStored.getId()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(6, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(originalStored.getId(), stored.getId()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(6, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applySetAmountFailUniqueMulti() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot add an amount to a unique item.", e.getMessage()); } - + @Test public void applySetAmountFailUniqueSingle() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot add an amount to a unique item.", e.getMessage()); } - + @Test public void applySetAmountFailBulkBlockNotInInventory() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + ObjectId badBlockId = new ObjectId(); ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .block(badBlockId) .build(); - + ValidationException e = assertThrows(ValidationException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Storage block " + badBlockId + " not used to hold this item (" + item.getId() + ").", e.getMessage()); } - + @Test public void applySetAmountFailBulkStoredNotInBlock() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + ObjectId badStoredId = new ObjectId(); ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .stored(badStoredId) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Stored given does not match stored found in block.", e.getMessage()); } - + @Test public void applySetAmountFailAmtListBadStored() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + ObjectId badStoredId = new ObjectId(); ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .stored(badStoredId) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Could not find Stored with id " + badStoredId, e.getMessage()); } - + @Test public void applySetAmountFailAmtListBadStoredNotInBlock() { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - - ObjectId origBlock = item.getStorageBlocks().getFirst(); + + ObjectId origBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId otherBlockId = this.storageBlockService.add(DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity).getId(); - - item.getStorageBlocks().add(otherBlockId); + + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(otherBlockId).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored originalStored = AmountStored.builder() .item(item.getId()) .state(StoredInBlock.builder().storageBlock(origBlock).build()) .amount(Quantities.getQuantity(5, item.getUnit())) .build(); this.storedService.add(DEFAULT_TEST_DB_NAME, originalStored, entity); - + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .stored(originalStored.getId()) .block(otherBlockId) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Stored given does not exist in block.", e.getMessage()); } - + @Test public void applySetAmountFailAmtListNewEntry() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + ItemStoredTransaction preApplyTransaction = SetAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .block(item.getStorageBlocks().getFirst()) + .block(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Must specify a stored to set the amount of.", e.getMessage()); } - + //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SubtractAmountAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SubtractAmountAppliedTransactionTest.java index 5fa90e4bc7..abe0dcc0e8 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SubtractAmountAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/SubtractAmountAppliedTransactionTest.java @@ -12,6 +12,7 @@ import tech.ebp.oqm.core.api.model.object.history.events.UpdateEvent; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -44,168 +45,168 @@ public class SubtractAmountAppliedTransactionTest extends AppliedTransactionServ public void applySubtractAmountSuccessBulkBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(), entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(0, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applySubtractAmountSuccessBulkStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(initialStored.getId()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(0, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applySubtractAmountSuccessList() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(initialStored.getId()) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(0, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - - + + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored storedFromSearch = (AmountStored) storedSearchResult.getResults().getFirst(); - + AmountStored stored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().stream().findFirst().get()); assertEquals(storedFromSearch, stored); assertEquals(Quantities.getQuantity(0, item.getUnit()), stored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(stored.getId())); assertFalse(storedHistory.isEmpty()); UpdateEvent event = (UpdateEvent) storedHistory.getResults().getFirst(); assertTrue(event.getDetails().containsKey(ITEM_TRANSACTION.name())); assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } - + @Test public void applySubtractAmountFailUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - + UniqueStored initialStored = UniqueStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(initialStored.getId()) .build(); IllegalArgumentException @@ -213,25 +214,25 @@ public void applySubtractAmountFailUniqueMulti() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot subtract an amount from a unique item.", e.getMessage()); } - + @Test public void applySubtractAmountFailUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - + UniqueStored initialStored = UniqueStored.builder() .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(initialStored.getId()) .build(); IllegalArgumentException @@ -239,26 +240,26 @@ public void applySubtractAmountFailUniqueSingle() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot subtract an amount from a unique item.", e.getMessage()); } - + @Test public void applySubtractAmountFailBulkSubMoreThanHeld() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(2, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(initialStored.getId()) .build(); IllegalArgumentException @@ -266,26 +267,26 @@ public void applySubtractAmountFailBulkSubMoreThanHeld() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Resulting amount less than zero. (subtracting 5 units from 2 units resulting in -3 units)", e.getMessage()); } - + @Test public void applySubtractAmountFailAmtListSubMoreThanHeld() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(2, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(initialStored.getId()) .build(); IllegalArgumentException @@ -293,21 +294,21 @@ public void applySubtractAmountFailAmtListSubMoreThanHeld() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Resulting amount less than zero. (subtracting 5 units from 2 units resulting in -3 units)", e.getMessage()); } - + @Test public void applySubtractAmountFailBulkMismatchedBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + StorageBlock newBlock = StorageBlock.builder().label(FAKER.location().building()).build(); this.storageBlockService.add(DEFAULT_TEST_DB_NAME, newBlock, entity); - item.getStorageBlocks().add(newBlock.getId()); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(newBlock.getId()).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -324,7 +325,7 @@ public void applySubtractAmountFailBulkMismatchedBlock() throws Exception { secondStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(newBlock.getId()) @@ -335,21 +336,21 @@ public void applySubtractAmountFailBulkMismatchedBlock() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Stored id in transaction not the id of stored found.", e.getMessage()); } - + @Test public void applySubtractAmountFailBulkMismatchedStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - + StorageBlock newBlock = StorageBlock.builder().label(FAKER.location().building()).build(); this.storageBlockService.add(DEFAULT_TEST_DB_NAME, newBlock, entity); - item.getStorageBlocks().add(newBlock.getId()); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(newBlock.getId()).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -366,10 +367,10 @@ public void applySubtractAmountFailBulkMismatchedStored() throws Exception { secondStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(secondStored.getId()) .build(); IllegalArgumentException @@ -377,21 +378,21 @@ public void applySubtractAmountFailBulkMismatchedStored() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Stored id in transaction not the id of stored found.", e.getMessage()); } - + @Test public void applySubtractAmountFailListMismatchedBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + StorageBlock newBlock = StorageBlock.builder().label(FAKER.location().building()).build(); this.storageBlockService.add(DEFAULT_TEST_DB_NAME, newBlock, entity); - item.getStorageBlocks().add(newBlock.getId()); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(newBlock.getId()).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -408,7 +409,7 @@ public void applySubtractAmountFailListMismatchedBlock() throws Exception { secondStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(newBlock.getId()) @@ -419,21 +420,21 @@ public void applySubtractAmountFailListMismatchedBlock() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Stored retrieved not in specified block.", e.getMessage()); } - + @Test public void applySubtractAmountFailListMismatchedStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + StorageBlock newBlock = StorageBlock.builder().label(FAKER.location().building()).build(); this.storageBlockService.add(DEFAULT_TEST_DB_NAME, newBlock, entity); - item.getStorageBlocks().add(newBlock.getId()); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(newBlock.getId()).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, @@ -450,10 +451,10 @@ public void applySubtractAmountFailListMismatchedStored() throws Exception { secondStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .fromStored(secondStored.getId()) .build(); IllegalArgumentException @@ -461,32 +462,32 @@ public void applySubtractAmountFailListMismatchedStored() throws Exception { assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Stored retrieved not in specified block.", e.getMessage()); } - + @Test public void applySubtractAmountFailNullStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - + AmountStored initialStored = AmountStored.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .item(item.getId()) - .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst()).build()) + .state(StoredInBlock.builder().storageBlock(item.getStorageBlocks().getFirst().getStorageBlock()).build()) .build(); this.storedService.add( DEFAULT_TEST_DB_NAME, initialStored, entity ); - + ItemStoredTransaction preApplyTransaction = SubAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) - .fromBlock(item.getStorageBlocks().getFirst()) + .fromBlock(item.getStorageBlocks().getFirst().getStorageBlock()) .build(); IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Must specify the stored item we are subtracting from.", e.getMessage()); } - + //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferAmountAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferAmountAppliedTransactionTest.java index 954c3bc402..21b4ef918a 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferAmountAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferAmountAppliedTransactionTest.java @@ -11,6 +11,7 @@ import tech.ebp.oqm.core.api.model.object.history.events.UpdateEvent; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -37,20 +38,20 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class TransferAmountAppliedTransactionTest extends AppliedTransactionServiceTest { - + @Test public void applyTransferAmountSuccessBulkNotInBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -59,36 +60,36 @@ public void applyTransferAmountSuccessBulkNotInBlock() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(2, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); //'first' and 'second' are flipped here AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); AmountStored secondStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(1); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -101,7 +102,7 @@ public void applyTransferAmountSuccessBulkNotInBlock() throws Exception { AmountStored secondStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getLast()); assertEquals(secondStoredFromSearch, secondStored); assertEquals(Quantities.getQuantity(0, item.getUnit()), secondStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(secondStored.getId())); @@ -111,20 +112,20 @@ public void applyTransferAmountSuccessBulkNotInBlock() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferAmountSuccessBulkInBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -141,36 +142,36 @@ public void applyTransferAmountSuccessBulkInBlock() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(2, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); //'first' and 'second' are flipped here AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); AmountStored secondStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(1); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -183,7 +184,7 @@ public void applyTransferAmountSuccessBulkInBlock() throws Exception { AmountStored secondStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getLast()); assertEquals(secondStoredFromSearch, secondStored); assertEquals(Quantities.getQuantity(0, item.getUnit()), secondStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(secondStored.getId())); @@ -193,20 +194,20 @@ public void applyTransferAmountSuccessBulkInBlock() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferAmountSuccessBulkFromBlockToStored() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -223,37 +224,37 @@ public void applyTransferAmountSuccessBulkFromBlockToStored() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .toStored(destinationStoredId) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(2, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); //'first' and 'second' are flipped here AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); AmountStored secondStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(1); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -266,7 +267,7 @@ public void applyTransferAmountSuccessBulkFromBlockToStored() throws Exception { AmountStored secondStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getLast()); assertEquals(secondStoredFromSearch, secondStored); assertEquals(Quantities.getQuantity(0, item.getUnit()), secondStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(secondStored.getId())); @@ -276,20 +277,20 @@ public void applyTransferAmountSuccessBulkFromBlockToStored() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferAmountSuccessBulkFromStoredToBlock() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -306,37 +307,37 @@ public void applyTransferAmountSuccessBulkFromStoredToBlock() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .fromStored(initialStoredId) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(2, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); //'first' and 'second' are flipped here AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); AmountStored secondStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(1); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -349,7 +350,7 @@ public void applyTransferAmountSuccessBulkFromStoredToBlock() throws Exception { AmountStored secondStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getLast()); assertEquals(secondStoredFromSearch, secondStored); assertEquals(Quantities.getQuantity(0, item.getUnit()), secondStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(secondStored.getId())); @@ -359,20 +360,20 @@ public void applyTransferAmountSuccessBulkFromStoredToBlock() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferAmountSuccessList() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -389,7 +390,7 @@ public void applyTransferAmountSuccessList() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) @@ -397,30 +398,30 @@ public void applyTransferAmountSuccessList() throws Exception { .toBlock(secondBlock) .toStored(destinationStoredId) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(2, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); //'first' and 'second' are flipped here AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); AmountStored secondStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(1); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -433,7 +434,7 @@ public void applyTransferAmountSuccessList() throws Exception { AmountStored secondStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getLast()); assertEquals(secondStoredFromSearch, secondStored); assertEquals(Quantities.getQuantity(0, item.getUnit()), secondStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(secondStored.getId())); @@ -443,20 +444,20 @@ public void applyTransferAmountSuccessList() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferAmountSuccessListToNonExistent() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -465,37 +466,37 @@ public void applyTransferAmountSuccessListToNonExistent() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .fromStored(initialStoredId) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(2, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(2, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 2); //'first' and 'second' are flipped here AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); AmountStored secondStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(1); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -508,7 +509,7 @@ public void applyTransferAmountSuccessListToNonExistent() throws Exception { AmountStored secondStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getLast()); assertEquals(secondStoredFromSearch, secondStored); assertEquals(Quantities.getQuantity(0, item.getUnit()), secondStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(secondStored.getId())); @@ -518,69 +519,69 @@ public void applyTransferAmountSuccessListToNonExistent() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - - + + @Test public void applyTransferAmountFailUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .build(); - - + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot subtract an amount from a unique item.", e.getMessage()); } - + @Test public void applyTransferAmountFailUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(5, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .build(); - - + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Cannot subtract an amount from a unique item.", e.getMessage()); } - + @Test public void applyTransferAmountFailBulkNotEnoughToTransfer() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -589,32 +590,32 @@ public void applyTransferAmountFailBulkNotEnoughToTransfer() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(6, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Resulting amount less than zero. (subtracting 6 units from 5 units resulting in -1 units)", e.getMessage()); } - + @Test public void applyTransferAmountFailAmtListNotEnoughToTransfer() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -623,33 +624,33 @@ public void applyTransferAmountFailAmtListNotEnoughToTransfer() throws Exception .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(6, item.getUnit())) .fromBlock(firstBlock) .fromStored(initialStoredId) .toBlock(secondBlock) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("Resulting amount less than zero. (subtracting 6 units from 5 units resulting in -1 units)", e.getMessage()); } - + @Test public void applyTransferAmountFailBulkMismatchedStoredId() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -666,33 +667,33 @@ public void applyTransferAmountFailBulkMismatchedStoredId() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(6, item.getUnit())) .fromBlock(firstBlock) .fromStored(destinationStoredId) .toBlock(secondBlock) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("From Stored retrieved not in specified block.", e.getMessage()); } - + @Test public void applyTransferAmountFailBulkMismatchedBlockTo() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -709,19 +710,19 @@ public void applyTransferAmountFailBulkMismatchedBlockTo() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferAmountTransaction.builder() .amount(Quantities.getQuantity(6, item.getUnit())) .fromBlock(firstBlock) .toBlock(secondBlock) .toStored(initialStoredId) .build(); - + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()->this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity)); assertEquals("To Stored retrieved not in specified block.", e.getMessage()); } - + //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferWholeAppliedTransactionTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferWholeAppliedTransactionTest.java index f66b2467fe..e8f36b3b00 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferWholeAppliedTransactionTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/mongo/transactions/transactionApplier/TransferWholeAppliedTransactionTest.java @@ -11,6 +11,7 @@ import tech.ebp.oqm.core.api.model.object.history.events.UpdateEvent; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; +import tech.ebp.oqm.core.api.model.object.storage.items.StorageBlockSettings; import tech.ebp.oqm.core.api.model.object.storage.items.StorageType; import tech.ebp.oqm.core.api.model.object.storage.items.stored.AmountStored; import tech.ebp.oqm.core.api.model.object.storage.items.stored.Stored; @@ -39,20 +40,20 @@ @QuarkusTest @QuarkusTestResource(value = KafkaCompanionResource.class, restrictToAnnotatedClass = true) public class TransferWholeAppliedTransactionTest extends AppliedTransactionServiceTest { - + @Test public void applyTransferWholeSuccessBulk() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.BULK, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -61,33 +62,33 @@ public void applyTransferWholeSuccessBulk() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferWholeTransaction.builder() .fromBlock(firstBlock) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -97,20 +98,20 @@ public void applyTransferWholeSuccessBulk() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferWholeSuccessAmtList() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.AMOUNT_LIST, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, AmountStored.builder() .item(item.getId()) @@ -119,34 +120,34 @@ public void applyTransferWholeSuccessAmtList() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferWholeTransaction.builder() .fromBlock(firstBlock) .storedToTransfer(initialStoredId) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(5, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); AmountStored firstStoredFromSearch = (AmountStored) storedSearchResult.getResults().get(0); - + { AmountStored firstStored = (AmountStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); assertEquals(Quantities.getQuantity(5, item.getUnit()), firstStored.getAmount()); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -156,20 +157,20 @@ public void applyTransferWholeSuccessAmtList() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - + @Test public void applyTransferWholeSuccessUniqueMulti() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_MULTI, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, UniqueStored.builder() .item(item.getId()) @@ -177,33 +178,33 @@ public void applyTransferWholeSuccessUniqueMulti() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferWholeTransaction.builder() .fromBlock(firstBlock) .storedToTransfer(initialStoredId) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); UniqueStored firstStoredFromSearch = (UniqueStored) storedSearchResult.getResults().get(0); - + { UniqueStored firstStored = (UniqueStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -213,21 +214,21 @@ public void applyTransferWholeSuccessUniqueMulti() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - - + + @Test public void applyTransferWholeSuccessUniqueSingle() throws Exception { InteractingEntity entity = this.getTestUserService().getTestUser(); InventoryItem item = setupItem(StorageType.UNIQUE_SINGLE, entity); - ObjectId firstBlock = item.getStorageBlocks().getFirst(); + ObjectId firstBlock = item.getStorageBlocks().getFirst().getStorageBlock(); ObjectId secondBlock = this.storageBlockService.add( DEFAULT_TEST_DB_NAME, StorageBlock.builder().label(FAKER.location().building()).build(), entity ).getId(); - item.getStorageBlocks().add(secondBlock); + item.getStorageBlocks().add(StorageBlockSettings.builder().storageBlock(secondBlock).build()); this.inventoryItemService.update(DEFAULT_TEST_DB_NAME, item, entity); - + ObjectId initialStoredId = this.storedService.add( DEFAULT_TEST_DB_NAME, UniqueStored.builder() .item(item.getId()) @@ -235,33 +236,33 @@ public void applyTransferWholeSuccessUniqueSingle() throws Exception { .build(), entity ).getId(); - + ItemStoredTransaction preApplyTransaction = TransferWholeTransaction.builder() .fromBlock(firstBlock) .storedToTransfer(initialStoredId) .toBlock(secondBlock) .build(); - + AppliedTransaction appliedTransaction = this.appliedTransactionService.apply(DEFAULT_TEST_DB_NAME, null, item, preApplyTransaction, entity); - + assertEquals(entity.getId(), appliedTransaction.getEntity()); assertEquals(item.getId(), appliedTransaction.getInventoryItem()); assertEquals(1, appliedTransaction.getAffectedStored().size()); assertEquals(preApplyTransaction, appliedTransaction.getTransaction()); assertTrue(appliedTransaction.getTimestamp().isBefore(ZonedDateTime.now())); - + assertEquals(1, appliedTransaction.getPostApplyResults().getStats().getNumStored()); assertEquals(Quantities.getQuantity(1, item.getUnit()), appliedTransaction.getPostApplyResults().getStats().getTotal()); //TODO:: storage block stats - + SearchResult storedSearchResult = this.storedService.search(DEFAULT_TEST_DB_NAME, new StoredSearch().setInventoryItemId(item.getId())); assertEquals(storedSearchResult.getNumResults(), 1); UniqueStored firstStoredFromSearch = (UniqueStored) storedSearchResult.getResults().get(0); - + { UniqueStored firstStored = (UniqueStored) this.storedService.get(DEFAULT_TEST_DB_NAME, appliedTransaction.getAffectedStored().getFirst()); assertEquals(firstStoredFromSearch, firstStored); - + SearchResult storedHistory = this.storedService.getHistoryService().search(DEFAULT_TEST_DB_NAME, new HistorySearch().setObjectId(firstStored.getId())); @@ -271,7 +272,7 @@ public void applyTransferWholeSuccessUniqueSingle() throws Exception { assertEquals(appliedTransaction.getId(), ((ItemTransactionDetail) event.getDetails().get(ITEM_TRANSACTION.name())).getInventoryItemTransaction()); } } - - + + //TODO:: any more? } diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeServiceTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeServiceTest.java index c5a7ea40fa..8d793d21cc 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeServiceTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/service/schemaVersioning/ObjectSchemaUpgradeServiceTest.java @@ -21,7 +21,6 @@ import tech.ebp.oqm.core.api.service.mongo.InteractingEntityService; import tech.ebp.oqm.core.api.service.mongo.InventoryItemService; import tech.ebp.oqm.core.api.service.mongo.ItemCheckoutService; -import tech.ebp.oqm.core.api.service.mongo.MongoService; import tech.ebp.oqm.core.api.service.mongo.StorageBlockService; import tech.ebp.oqm.core.api.service.mongo.StoredService; import tech.ebp.oqm.core.api.testResources.testClasses.RunningServerTest; @@ -67,7 +66,7 @@ public class ObjectSchemaUpgradeServiceTest extends RunningServerTest { CoreApiInteractingEntity coreApiInteractingEntity; @Inject - ObjectSchemaUpgradeService objectSchemaUpgradeService; + ObjectSchemaUpgradeService objectSchemaUpgradeService; public static Stream tests() throws IOException { List tests = new ArrayList<>(); diff --git a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/testResources/testClasses/RunningServerTest.java b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/testResources/testClasses/RunningServerTest.java index 7bfa81bff0..76ddc7206c 100644 --- a/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/testResources/testClasses/RunningServerTest.java +++ b/software/core/oqm-core-api/src/test/java/tech/ebp/oqm/core/api/testResources/testClasses/RunningServerTest.java @@ -1,7 +1,6 @@ package tech.ebp.oqm.core.api.testResources.testClasses; import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.smallrye.reactive.messaging.kafka.companion.ConsumerBuilder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.microprofile.config.ConfigProvider; @@ -11,12 +10,9 @@ import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import tech.ebp.oqm.core.api.model.object.interactingEntity.user.User; -import tech.ebp.oqm.core.api.service.notification.HistoryEventNotificationService; import tech.ebp.oqm.core.api.testResources.data.MongoTestConnector; import tech.ebp.oqm.core.api.testResources.data.TestUserService; -import java.time.Duration; - import static io.restassured.RestAssured.given; import static tech.ebp.oqm.core.api.testResources.TestConstants.DEFAULT_TEST_DB_NAME; import static tech.ebp.oqm.core.api.testResources.TestRestUtils.setupJwtCall; @@ -25,9 +21,12 @@ @Execution(ExecutionMode.SAME_THREAD) public abstract class RunningServerTest extends WebServerTest { + private boolean needDbReset = true; + private boolean needKafkaReset = true; + @Getter TestUserService testUserService = TestUserService.getInstance(); - + public boolean isIntTest(){ return getClass().isAnnotationPresent(QuarkusIntegrationTest.class); } @@ -35,7 +34,7 @@ public boolean isIntTest(){ @BeforeEach public void beforeEach(TestInfo testInfo){ log.info("Running before method for test {}", testInfo.getDisplayName()); - + User adminUser = this.getTestUserService().getTestUser(true); setupJwtCall(given(), this.getTestUserService().getUserToken(adminUser)) .basePath("") @@ -43,30 +42,51 @@ public void beforeEach(TestInfo testInfo){ setupJwtCall(given(), this.getTestUserService().getUserToken(adminUser)) .basePath("") .put("/api/v1/inventory/manage/db/ensure/" + DEFAULT_TEST_DB_NAME).then().statusCode(200); - + //clear kafka queues if(this instanceof KafkaTest){ ((KafkaTest)this).clearKafkaQueues(log); } } - + @AfterEach public void afterEach( TestInfo testInfo ) { log.info("Running after method for test {}", testInfo.getDisplayName()); - - if(ConfigProvider.getConfig().getOptionalValue("quarkus.mongodb.connection-string", String.class).isEmpty()){ - log.info("Mongo not started."); + + if(this.needDbReset){ + if(ConfigProvider.getConfig().getOptionalValue("quarkus.mongodb.connection-string", String.class).isEmpty()){ + log.info("Mongo not started."); + } else { + MongoTestConnector.getInstance(this.isIntTest()).clearDb(); + } } else { - MongoTestConnector.getInstance(this.isIntTest()).clearDb(); + log.info("Skipping db reset."); } - - //clear kafka queues - if(this instanceof KafkaTest){ - ((KafkaTest)this).clearKafkaQueues(log); + + if(this.needKafkaReset) { + //clear kafka queues + if (this instanceof KafkaTest) { + ((KafkaTest) this).clearKafkaQueues(log); + } + } else { + log.info("Skipping kafka reset."); } - + log.info("Completed after step."); } + + protected void setNeedDbReset(boolean needDbReset){ + this.needDbReset = needDbReset; + } + protected void setNeedKafkaReset(boolean needKafkaReset){ + this.needKafkaReset = needKafkaReset; + } + + protected void setNeedResets(boolean needReset){ + this.setNeedDbReset(needReset); + this.setNeedKafkaReset(needReset); + } + } diff --git a/software/core/oqm-core-base-station/build.gradle b/software/core/oqm-core-base-station/build.gradle index ad227e4052..11eac30cbd 100644 --- a/software/core/oqm-core-base-station/build.gradle +++ b/software/core/oqm-core-base-station/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'com.ebp.openQuarterMaster' -version '1.18.1' +version '1.19.0-SNAPSHOT' repositories { mavenCentral() @@ -32,12 +32,12 @@ dependencies { implementation 'io.quarkus:quarkus-scheduler' implementation 'tech.epic-breakfast-productions.openquartermaster.lib.core:core-characteristics-lib-quarkus:1.0.1' - implementation 'tech.epic-breakfast-productions.openquartermaster.lib.core:core-api-lib-quarkus:5.0.0' + implementation 'tech.epic-breakfast-productions.openquartermaster.lib.core:core-api-lib-quarkus:6.0.0-SNAPSHOT' implementation 'org.apache.commons:commons-io:1.3.2' implementation 'org.apache.commons:commons-text:1.15.0' - implementation 'uk.org.okapibarcode:okapibarcode:0.5.5' + implementation 'uk.org.okapibarcode:okapibarcode:0.5.6' implementation 'com.itextpdf:html2pdf:6.3.2' //webjars diff --git a/software/core/oqm-core-base-station/installerSrc/installerProperties.json b/software/core/oqm-core-base-station/installerSrc/installerProperties.json index 4d3389cfe4..bb85dbcd24 100644 --- a/software/core/oqm-core-base-station/installerSrc/installerProperties.json +++ b/software/core/oqm-core-base-station/installerSrc/installerProperties.json @@ -6,7 +6,7 @@ "description": "Base Station instance for Open QuarterMaster.", "homepage": "https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/tree/main/software/open-qm-base-station", "dependencies": { - "deb": "curl, jq, docker.io, oqm-manager-station+captain (>= 2.11.0), oqm-infra-keycloak (>= 2.0.0), oqm-infra-traefik (>= 1.0.0), oqm-core-api (>= 5.0.0), oqm-core-characteristics (>= 1.0.1)", + "deb": "curl, jq, docker.io, oqm-manager-station+captain (>= 2.11.0), oqm-infra-keycloak (>= 2.0.0), oqm-infra-traefik (>= 1.0.0), oqm-core-api (>= 6.0.0), oqm-core-characteristics (>= 1.0.1)", "debRec": "oqm-plugin-ext_item_search" }, "copyright": { diff --git a/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/model/UserInfo.java b/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/model/UserInfo.java index da04ae7577..bfd8b3969b 100644 --- a/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/model/UserInfo.java +++ b/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/model/UserInfo.java @@ -12,7 +12,7 @@ @NoArgsConstructor @AllArgsConstructor public class UserInfo { - + private String id; private String authId; private String name; diff --git a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/lib/overtype/2.3.10/overtype.min.js b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/lib/overtype/2.3.10/overtype.min.js new file mode 100644 index 0000000000..d84ffc4a99 --- /dev/null +++ b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/lib/overtype/2.3.10/overtype.min.js @@ -0,0 +1,104 @@ +/** + * OverType v2.3.10 + * A lightweight markdown editor library with perfect WYSIWYG alignment + * @license MIT + * @author David Miranda + * https://github.com/panphora/overtype + */ +var OverType=(()=>{var ke=Object.defineProperty;var yn=Object.getOwnPropertyDescriptor;var wn=Object.getOwnPropertyNames;var bn=Object.prototype.hasOwnProperty;var xn=(t,e,n)=>e in t?ke(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n;var st=(t,e)=>{for(var n in e)ke(t,n,{get:e[n],enumerable:!0})},kn=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of wn(e))!bn.call(t,i)&&i!==n&&ke(t,i,{get:()=>e[i],enumerable:!(o=yn(e,i))||o.enumerable});return t};var Ln=t=>kn(ke({},"__esModule",{value:!0}),t);var T=(t,e,n)=>(xn(t,typeof e!="symbol"?e+"":e,n),n);var vo={};st(vo,{OverType:()=>j,default:()=>go,defaultToolbarButtons:()=>re,markdownActions:()=>ne,toolbarButtons:()=>S});var C=class{static resetLinkIndex(){this.linkIndex=0}static setCodeHighlighter(e){this.codeHighlighter=e}static setCustomSyntax(e){this.customSyntax=e}static applyCustomSyntax(e){return this.customSyntax?this.customSyntax(e):e}static escapeHtml(e){let n={"&":"&","<":"<",">":">",'"':""","'":"'"};return e.replace(/[&<>"']/g,o=>n[o])}static preserveIndentation(e,n){let i=n.match(/^(\s*)/)[1].replace(/ /g," ");return e.replace(/^\s*/,i)}static parseHeader(e){return e.replace(/^(#{1,3})\s(.+)$/,(n,o,i)=>{let r=o.length;return i=this.parseInlineElements(i),`${o} ${i}`})}static parseHorizontalRule(e){return e.match(/^(-{3,}|\*{3,}|_{3,})$/)?`

${e}
`:null}static parseBlockquote(e){return e.replace(/^> (.+)$/,(n,o)=>`> ${o}`)}static parseBulletList(e){return e.replace(/^((?: )*)([-*+])\s(.+)$/,(n,o,i,r)=>(r=this.parseInlineElements(r),`${o}
  • ${i} ${r}
  • `))}static parseTaskList(e,n=!1){return e.replace(/^((?: )*)-(\s+)\[([ xX])\](\s*)(.*)$/,(o,i,r,s,a,l)=>{if(l=this.parseInlineElements(l),n){let p=s.toLowerCase()==="x";return`${i}
  • ${l}
  • `}else return`${i}
  • -${r}[${s}]${a}${l}
  • `})}static parseNumberedList(e){return e.replace(/^((?: )*)(\d+\.)\s(.+)$/,(n,o,i,r)=>(r=this.parseInlineElements(r),`${o}
  • ${i} ${r}
  • `))}static parseCodeBlock(e){return/^`{3}[^`]*$/.test(e)?`
    ${e}
    `:null}static parseBold(e){return e=e.replace(/\*\*(.+?)\*\*/g,'**$1**'),e=e.replace(/__(.+?)__/g,'__$1__'),e}static parseItalic(e){return e=e.replace(new RegExp("(?])\\*(?!\\*)(.+?)(?*$1*'),e=e.replace(new RegExp("(?<=^|\\s)_(?!_)(.+?)(?_$1_'),e}static parseStrikethrough(e){return e=e.replace(new RegExp("(?~~$1~~'),e=e.replace(new RegExp("(?~$1~'),e}static parseInlineCode(e){return e.replace(new RegExp("(?$1$2$3')}static sanitizeUrl(e){let n=e.trim(),o=n.toLowerCase(),r=["http://","https://","mailto:","ftp://","ftps://"].some(a=>o.startsWith(a)),s=n.startsWith("/")||n.startsWith("#")||n.startsWith("?")||n.startsWith(".")||!n.includes(":")&&!n.includes("//");return r||s?e:"#"}static parseLinks(e){return e.replace(/\[(.+?)\]\((.+?)\)/g,(n,o,i)=>{let r=`--link-${this.linkIndex++}`;return`[${o}](${i})`})}static identifyAndProtectSanctuaries(e){let n=new Map,o=0,i=e,r=[],s=/\[([^\]]+)\]\(([^)]+)\)/g,a;for(;(a=s.exec(e))!==null;){let u=a.index+a[0].indexOf("](")+2,h=u+a[2].length;r.push({start:u,end:h})}let l=new RegExp("(?d>=f.start&&u<=f.end)||c.push({match:p[0],index:p.index,openTicks:p[1],content:p[2],closeTicks:p[3]})}return c.sort((d,u)=>u.index-d.index),c.forEach(d=>{let u=`\uE000${o++}\uE001`;n.set(u,{type:"code",original:d.match,openTicks:d.openTicks,content:d.content,closeTicks:d.closeTicks}),i=i.substring(0,d.index)+u+i.substring(d.index+d.match.length)}),i=i.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(d,u,h)=>{let f=`\uE000${o++}\uE001`;return n.set(f,{type:"link",original:d,linkText:u,url:h}),f}),{protectedText:i,sanctuaries:n}}static restoreAndTransformSanctuaries(e,n){return Array.from(n.keys()).sort((i,r)=>{let s=e.indexOf(i),a=e.indexOf(r);return s-a}).forEach(i=>{let r=n.get(i),s;if(r.type==="code")s=`${r.openTicks}${r.content}${r.closeTicks}`;else if(r.type==="link"){let a=r.linkText;n.forEach((c,d)=>{if(a.includes(d)&&c.type==="code"){let u=`${c.openTicks}${c.content}${c.closeTicks}`;a=a.replace(d,u)}}),a=this.parseStrikethrough(a),a=this.parseBold(a),a=this.parseItalic(a);let l=`--link-${this.linkIndex++}`;s=`[${a}](${r.url})`}e=e.replace(i,s)}),e}static parseInlineElements(e){let{protectedText:n,sanctuaries:o}=this.identifyAndProtectSanctuaries(e),i=n;return i=this.parseStrikethrough(i),i=this.parseBold(i),i=this.parseItalic(i),i=this.restoreAndTransformSanctuaries(i,o),i}static parseLine(e,n=!1){let o=this.escapeHtml(e);o=this.preserveIndentation(o,e);let i=this.parseHorizontalRule(o);if(i)return i;let r=this.parseCodeBlock(o);return r||(o=this.parseHeader(o),o=this.parseBlockquote(o),o=this.parseTaskList(o,n),o=this.parseBulletList(o),o=this.parseNumberedList(o),!o.includes(" ":`
    ${o}
    `)}static parse(e,n=-1,o=!1,i,r=!1){this.resetLinkIndex();let s=e.split(` +`),a=!1,p=s.map((c,d)=>{if(o&&d===n)return`
    ${this.escapeHtml(c)||" "}
    `;if(/^```[^`]*$/.test(c))return a=!a,this.applyCustomSyntax(this.parseLine(c,r));if(a){let h=this.escapeHtml(c);return`
    ${this.preserveIndentation(h,c)||" "}
    `}return this.applyCustomSyntax(this.parseLine(c,r))}).join("");return this.postProcessHTML(p,i)}static postProcessHTML(e,n){if(typeof document>"u"||!document)return this.postProcessHTMLManual(e,n);let o=document.createElement("div");o.innerHTML=e;let i=null,r=null,s=null,a=!1,l=Array.from(o.children);for(let p=0;p0&&(s._codeContent+=` +`);let f=c.textContent.replace(/\u00A0/g," ");s._codeContent+=f,h.textContent.length>0&&(h.textContent+=` +`),h.textContent+=f,c.remove();continue}let u=null;if(c.tagName==="DIV"&&(u=c.querySelector("li")),u){let h=u.classList.contains("bullet-list"),f=u.classList.contains("ordered-list");if(!h&&!f){i=null,r=null;continue}let m=h?"ul":"ol";(!i||r!==m)&&(i=document.createElement(m),o.insertBefore(i,c),r=m);let g=[];for(let y of c.childNodes)if(y.nodeType===3&&y.textContent.match(/^\u00A0+$/))g.push(y.cloneNode(!0));else if(y===u)break;g.forEach(y=>{u.insertBefore(y,u.firstChild)}),i.appendChild(u),c.remove()}else i=null,r=null}return o.innerHTML}static postProcessHTMLManual(e,n){let o=e;o=o.replace(/((?:
    (?: )*
  • .*?<\/li><\/div>\s*)+)/gs,r=>{let s=r.match(/
    (?: )*
  • .*?<\/li><\/div>/gs)||[];return s.length>0?"
      "+s.map(l=>{let p=l.match(/
      ((?: )*)
    • .*?<\/li>/);if(p&&c){let d=p[1];return c[0].replace(/
    • /,`
    • ${d}`)}return c?c[0]:""}).filter(Boolean).join("")+"
    ":r}),o=o.replace(/((?:
    (?: )*
  • .*?<\/li><\/div>\s*)+)/gs,r=>{let s=r.match(/
    (?: )*
  • .*?<\/li><\/div>/gs)||[];return s.length>0?"
      "+s.map(l=>{let p=l.match(/
      ((?: )*)
    1. .*?<\/li>/);if(p&&c){let d=p[1];return c[0].replace(/
    2. /,`
    3. ${d}`)}return c?c[0]:""}).filter(Boolean).join("")+"
    ":r});let i=/
    (```[^<]*)<\/span><\/div>(.*?)
    (```)<\/span><\/div>/gs;return o=o.replace(i,(r,s,a,l)=>{let c=(a.match(/
    (.*?)<\/div>/gs)||[]).map(g=>g.replace(/
    (.*?)<\/div>/s,"$1").replace(/ /g," ")).join(` +`),d=s.slice(3).trim(),u=d?` class="language-${d}"`:"",h=c,f=n||this.codeHighlighter;if(f)try{let g=c.replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&"),y=f(g,d);y&&typeof y.then=="function"?console.warn("Async highlighters are not supported in Node.js (non-DOM) context. Use synchronous highlighters for server-side rendering."):y&&typeof y=="string"&&y.trim()&&(h=y)}catch(g){console.warn("Code highlighting failed:",g)}let m=`
    ${s}
    `;return m+=`
    ${h}
    `,m+=`
    ${l}
    `,m}),o}static getListContext(e,n){let o=e.split(` +`),i=0,r=0,s=0;for(let u=0;u=n){r=u,s=i;break}i+=h+1}let a=o[r],l=s+a.length,p=a.match(this.LIST_PATTERNS.checkbox);if(p)return{inList:!0,listType:"checkbox",indent:p[1],marker:"-",checked:p[2]==="x",content:p[3],lineStart:s,lineEnd:l,markerEndPos:s+p[1].length+p[2].length+5};let c=a.match(this.LIST_PATTERNS.bullet);if(c)return{inList:!0,listType:"bullet",indent:c[1],marker:c[2],content:c[3],lineStart:s,lineEnd:l,markerEndPos:s+c[1].length+c[2].length+1};let d=a.match(this.LIST_PATTERNS.numbered);return d?{inList:!0,listType:"numbered",indent:d[1],marker:parseInt(d[2]),content:d[3],lineStart:s,lineEnd:l,markerEndPos:s+d[1].length+d[2].length+2}:{inList:!1,listType:null,indent:"",marker:null,content:a,lineStart:s,lineEnd:l,markerEndPos:s}}static createNewListItem(e){switch(e.listType){case"bullet":return`${e.indent}${e.marker} `;case"numbered":return`${e.indent}${e.marker+1}. `;case"checkbox":return`${e.indent}- [ ] `;default:return""}}static renumberLists(e){let n=e.split(` +`),o=new Map,i=!1;return n.map(s=>{let a=s.match(this.LIST_PATTERNS.numbered);if(a){let l=a[1],p=l.length,c=a[3];i||o.clear();let d=(o.get(p)||0)+1;o.set(p,d);for(let[u]of o)u>p&&o.delete(u);return i=!0,`${l}${d}. ${c}`}else return(s.trim()===""||!s.match(/^\s/))&&(i=!1,o.clear()),s}).join(` +`)}};T(C,"linkIndex",0),T(C,"codeHighlighter",null),T(C,"customSyntax",null),T(C,"LIST_PATTERNS",{bullet:/^(\s*)([-*+])\s+(.*)$/,numbered:/^(\s*)(\d+)\.\s+(.*)$/,checkbox:/^(\s*)-\s+\[([ x])\]\s+(.*)$/});var ae=class{constructor(e){this.editor=e}handleKeydown(e){if(!(navigator.platform.toLowerCase().includes("mac")?e.metaKey:e.ctrlKey))return!1;let i=null;switch(e.key.toLowerCase()){case"b":e.shiftKey||(i="toggleBold");break;case"i":e.shiftKey||(i="toggleItalic");break;case"k":e.shiftKey||(i="insertLink");break;case"7":e.shiftKey&&(i="toggleNumberedList");break;case"8":e.shiftKey&&(i="toggleBulletList");break}return i?(e.preventDefault(),this.editor.performAction(i,e),!0):!1}destroy(){}};var F={name:"solar",colors:{bgPrimary:"#faf0ca",bgSecondary:"#ffffff",text:"#0d3b66",textPrimary:"#0d3b66",textSecondary:"#5a7a9b",h1:"#f95738",h2:"#ee964b",h3:"#3d8a51",strong:"#ee964b",em:"#f95738",del:"#ee964b",link:"#0d3b66",code:"#0d3b66",codeBg:"rgba(244, 211, 94, 0.4)",blockquote:"#5a7a9b",hr:"#5a7a9b",syntaxMarker:"rgba(13, 59, 102, 0.52)",syntax:"#999999",cursor:"#f95738",selection:"rgba(244, 211, 94, 0.4)",listMarker:"#ee964b",rawLine:"#5a7a9b",border:"#e0e0e0",hoverBg:"#f0f0f0",primary:"#0d3b66",toolbarBg:"#ffffff",toolbarIcon:"#0d3b66",toolbarHover:"#f5f5f5",toolbarActive:"#faf0ca",placeholder:"#999999"},previewColors:{text:"#0d3b66",h1:"inherit",h2:"inherit",h3:"inherit",strong:"inherit",em:"inherit",link:"#0d3b66",code:"#0d3b66",codeBg:"rgba(244, 211, 94, 0.4)",blockquote:"#5a7a9b",hr:"#5a7a9b",bg:"transparent"}},at={name:"cave",colors:{bgPrimary:"#141E26",bgSecondary:"#1D2D3E",text:"#c5dde8",textPrimary:"#c5dde8",textSecondary:"#9fcfec",h1:"#d4a5ff",h2:"#f6ae2d",h3:"#9fcfec",strong:"#f6ae2d",em:"#9fcfec",del:"#f6ae2d",link:"#9fcfec",code:"#c5dde8",codeBg:"#1a232b",blockquote:"#9fcfec",hr:"#c5dde8",syntaxMarker:"rgba(159, 207, 236, 0.73)",syntax:"#7a8c98",cursor:"#f26419",selection:"rgba(51, 101, 138, 0.4)",listMarker:"#f6ae2d",rawLine:"#9fcfec",border:"#2a3f52",hoverBg:"#243546",primary:"#9fcfec",toolbarBg:"#1D2D3E",toolbarIcon:"#c5dde8",toolbarHover:"#243546",toolbarActive:"#2a3f52",placeholder:"#6a7a88"},previewColors:{text:"#c5dde8",h1:"inherit",h2:"inherit",h3:"inherit",strong:"inherit",em:"inherit",link:"#9fcfec",code:"#c5dde8",codeBg:"#1a232b",blockquote:"#9fcfec",hr:"#c5dde8",bg:"transparent"}},lt={solar:F,cave:at,auto:F,light:F,dark:at};function U(t){return typeof t=="string"?{...lt[t]||lt.solar,name:t}:t}function qe(t){if(t!=="auto")return t;let e=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)");return e!=null&&e.matches?"cave":"solar"}function Q(t,e){let n=[];for(let[o,i]of Object.entries(t)){let r=o.replace(/([A-Z])/g,"-$1").toLowerCase();n.push(`--${r}: ${i};`)}if(e)for(let[o,i]of Object.entries(e)){let r=o.replace(/([A-Z])/g,"-$1").toLowerCase();n.push(`--preview-${r}-default: ${i};`)}return n.join(` +`)}function ct(t,e={},n={}){return{...t,colors:{...t.colors,...e},previewColors:{...t.previewColors,...n}}}function pt(t={}){let{fontSize:e="14px",lineHeight:n=1.6,fontFamily:o='"SF Mono", SFMono-Regular, Menlo, Monaco, "Cascadia Code", Consolas, "Roboto Mono", "Noto Sans Mono", "Droid Sans Mono", "Ubuntu Mono", "DejaVu Sans Mono", "Liberation Mono", "Courier New", Courier, monospace',padding:i="20px",theme:r=null,mobile:s={}}=t,a=Object.keys(s).length>0?` + @media (max-width: 640px) { + .overtype-wrapper .overtype-input, + .overtype-wrapper .overtype-preview { + ${Object.entries(s).map(([p,c])=>`${p.replace(/([A-Z])/g,"-$1").toLowerCase()}: ${c} !important;`).join(` + `)} + } + } + `:"",l=r&&r.colors?Q(r.colors,r.previewColors):"";return`.overtype-container *{margin: 0 !important;padding: 0 !important;border: 0 !important;float: none !important;clear: none !important;text-decoration: none !important;text-transform: none !important;letter-spacing: normal !important;box-shadow: none !important;text-shadow: none !important;box-sizing: border-box !important}.overtype-container{display: flex !important;flex-direction: column !important;width: 100% !important;height: 100% !important;position: relative !important;overflow: visible !important;font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif !important;text-align: left !important;${l?` + /* Theme Variables */ + ${l}`:""}}.overtype-container .overtype-wrapper *{text-align: left !important}.overtype-container.overtype-auto-resize{height: auto !important}.overtype-container.overtype-auto-resize .overtype-wrapper{flex: 0 0 auto !important;height: auto !important;min-height: 60px !important;overflow: visible !important}.overtype-wrapper{position: relative !important;width: 100% !important;flex: 1 1 0 !important;min-height: 60px !important;overflow: hidden !important;background: var(--bg-secondary,#ffffff) !important;z-index: 1}.overtype-wrapper .overtype-input,.overtype-wrapper .overtype-preview{position: absolute !important;top: 0 !important;left: 0 !important;width: 100% !important;height: 100% !important;font-family: var(--instance-font-family,${o}) !important; + font-variant-ligatures: none !important; /* keep metrics stable for code */ + font-size: var(--instance-font-size, ${e}) !important; + line-height: var(--instance-line-height, ${n}) !important; + font-weight: normal !important; + font-style: normal !important; + font-variant: normal !important; + font-stretch: normal !important; + font-kerning: none !important; + font-feature-settings: normal !important; + + /* Box model - must match exactly */ + padding: var(--instance-padding, ${i}) !important;margin: 0 !important;border: none !important;outline: none !important;box-sizing: border-box !important;white-space: pre-wrap !important;word-wrap: break-word !important;word-break: normal !important;overflow-wrap: break-word !important;tab-size: 2 !important;-moz-tab-size: 2 !important;text-align: left !important;text-indent: 0 !important;letter-spacing: normal !important;word-spacing: normal !important;text-transform: none !important;text-rendering: auto !important;-webkit-font-smoothing: auto !important;-webkit-text-size-adjust: 100% !important;direction: ltr !important;writing-mode: horizontal-tb !important;unicode-bidi: normal !important;text-orientation: mixed !important;text-shadow: none !important;filter: none !important;transform: none !important;zoom: 1 !important;vertical-align: baseline !important;min-width: 0 !important;min-height: 0 !important;max-width: none !important;max-height: none !important;overflow-y: auto !important;overflow-x: auto !important;scrollbar-width: auto !important;scrollbar-gutter: auto !important;animation: none !important;transition: none !important}.overtype-wrapper .overtype-input{z-index: 1 !important;color: transparent !important;caret-color: var(--cursor,#f95738) !important;background-color: transparent !important;resize: none !important;appearance: none !important;-webkit-appearance: none !important;-moz-appearance: none !important;touch-action: manipulation !important;autocomplete: off !important;autocorrect: off !important;autocapitalize: off !important}.overtype-wrapper .overtype-input::selection{background-color: var(--selection,rgba(244,211,94,0.4))}.overtype-wrapper .overtype-placeholder{position: absolute !important;top: 0 !important;left: 0 !important;width: 100% !important;z-index: 0 !important;pointer-events: none !important;user-select: none !important;font-family: var(--instance-font-family,${o}) !important; + font-size: var(--instance-font-size, ${e}) !important; + line-height: var(--instance-line-height, ${n}) !important; + padding: var(--instance-padding, ${i}) !important;box-sizing: border-box !important;color: var(--placeholder,#999) !important}.overtype-wrapper .overtype-preview{z-index: 0 !important;pointer-events: none !important;color: var(--text,#0d3b66) !important;background-color: transparent !important;user-select: none !important;-webkit-user-select: none !important;-moz-user-select: none !important;-ms-user-select: none !important}.overtype-wrapper .overtype-preview *{font-family: inherit !important;font-size: inherit !important;line-height: inherit !important}.overtype-wrapper .overtype-preview div{margin: 0 !important;padding: 0 !important;border: none !important;text-align: left !important;text-indent: 0 !important;display: block !important;position: static !important;transform: none !important;min-height: 0 !important;max-height: none !important;line-height: inherit !important;font-size: inherit !important;font-family: inherit !important}.overtype-wrapper .overtype-preview .header{font-weight: bold !important}.overtype-wrapper .overtype-preview .h1{color: var(--h1,#f95738) !important}.overtype-wrapper .overtype-preview .h2{color: var(--h2,#ee964b) !important}.overtype-wrapper .overtype-preview .h3{color: var(--h3,#3d8a51) !important}.overtype-wrapper .overtype-preview h1,.overtype-wrapper .overtype-preview h2,.overtype-wrapper .overtype-preview h3{font-size: inherit !important;font-weight: bold !important;margin: 0 !important;padding: 0 !important;display: inline !important;line-height: inherit !important}.overtype-wrapper .overtype-preview h1{color: var(--h1,#f95738) !important}.overtype-wrapper .overtype-preview h2{color: var(--h2,#ee964b) !important}.overtype-wrapper .overtype-preview h3{color: var(--h3,#3d8a51) !important}.overtype-wrapper .overtype-preview ul,.overtype-wrapper .overtype-preview ol{list-style: none !important;margin: 0 !important;padding: 0 !important;display: block !important}.overtype-wrapper .overtype-preview li{display: block !important;margin: 0 !important;padding: 0 !important}.overtype-wrapper .overtype-preview strong{color: var(--strong,#ee964b) !important;font-weight: bold !important}.overtype-wrapper .overtype-preview em{color: var(--em,#f95738) !important;text-decoration-color: var(--em,#f95738) !important;text-decoration-thickness: 1px !important;font-style: italic !important}.overtype-wrapper .overtype-preview del{color: var(--del,#ee964b) !important;text-decoration: line-through !important;text-decoration-color: var(--del,#ee964b) !important;text-decoration-thickness: 1px !important}.overtype-wrapper .overtype-preview code{background: var(--code-bg,rgba(244,211,94,0.4)) !important;color: var(--code,#0d3b66) !important;padding: 0 !important;border-radius: 2px !important;font-family: inherit !important;font-size: inherit !important;line-height: inherit !important;font-weight: normal !important}.overtype-wrapper .overtype-preview pre{padding: 0 !important;margin: 0 !important;border-radius: 4px !important;overflow-x: auto !important}.overtype-wrapper .overtype-preview pre.code-block{background: var(--code-bg,rgba(244,211,94,0.4)) !important;white-space: break-spaces !important}.overtype-wrapper .overtype-preview pre code{background: transparent !important;color: var(--code,#0d3b66) !important;font-family: var(--instance-font-family,${o}) !important}.overtype-wrapper .overtype-preview .blockquote{color: var(--blockquote,#5a7a9b) !important;padding: 0 !important;margin: 0 !important;border: none !important}.overtype-wrapper .overtype-preview a{color: var(--link,#0d3b66) !important;text-decoration: underline !important;font-weight: normal !important}.overtype-wrapper .overtype-preview a:hover{text-decoration: underline !important;color: var(--link,#0d3b66) !important}.overtype-wrapper .overtype-preview ul,.overtype-wrapper .overtype-preview ol{list-style: none !important;margin: 0 !important;padding: 0 !important}.overtype-wrapper .overtype-preview hr{border: none !important;color: var(--hr,#5a7a9b) !important;margin: 0 !important;padding: 0 !important}.overtype-wrapper .overtype-preview .hr-marker{color: var(--hr,#5a7a9b) !important;opacity: 0.6 !important}.overtype-wrapper .overtype-preview .code-fence{color: var(--code,#0d3b66) !important;background: var(--code-bg,rgba(244,211,94,0.4)) !important}.overtype-wrapper .overtype-preview .code-block-line{background: var(--code-bg,rgba(244,211,94,0.4)) !important}.overtype-wrapper .overtype-preview .code-block-line .code-fence{background: transparent !important}.overtype-wrapper .overtype-preview .raw-line{color: var(--raw-line,#5a7a9b) !important;font-style: normal !important;font-weight: normal !important}.overtype-wrapper .overtype-preview .syntax-marker{color: var(--syntax-marker,rgba(13,59,102,0.52)) !important;opacity: 0.7 !important}.overtype-wrapper .overtype-preview .list-marker{color: var(--list-marker,#ee964b) !important}.overtype-stats{height: 40px !important;padding: 0 20px !important;background: var(--bg-secondary,#f8f9fa) !important;border-top: 1px solid var(--border,#e0e0e0) !important;display: flex !important;justify-content: space-between !important;align-items: center !important;font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif !important;font-size: 0.85rem !important;color: var(--text-secondary,#666) !important;flex-shrink: 0 !important;z-index: 10001 !important;position: relative !important}.overtype-stats .overtype-stat{display: flex !important;align-items: center !important;gap: 5px !important;white-space: nowrap !important}.overtype-stats .live-dot{width: 8px !important;height: 8px !important;background: #4caf50 !important;border-radius: 50% !important;animation: overtype-pulse 2s infinite !important}@keyframes overtype-pulse{0%,100%{opacity: 1;transform: scale(1)}50%{opacity: 0.6;transform: scale(1.2)}}.overtype-toolbar.overtype-toolbar-hidden{display: none !important}.overtype-toolbar{display: flex !important;align-items: center !important;gap: 4px !important;padding: 8px !important;background: var(--toolbar-bg,var(--bg-primary,#f8f9fa)) !important;border-bottom: 1px solid var(--toolbar-border,transparent) !important;overflow-x: auto !important;overflow-y: hidden !important;-webkit-overflow-scrolling: touch !important;flex-shrink: 0 !important;height: auto !important;position: relative !important;z-index: 100 !important;scrollbar-width: thin}.overtype-toolbar::-webkit-scrollbar{height: 4px}.overtype-toolbar::-webkit-scrollbar-track{background: transparent}.overtype-toolbar::-webkit-scrollbar-thumb{background: rgba(0,0,0,0.2);border-radius: 2px}.overtype-toolbar-button{display: flex;align-items: center;justify-content: center;width: 32px;height: 32px;padding: 0;border: none;border-radius: 6px;background: transparent;color: var(--toolbar-icon,var(--text-secondary,#666));cursor: pointer;transition: all 0.2s ease;flex-shrink: 0}.overtype-toolbar-button svg{width: 20px;height: 20px;fill: currentColor}.overtype-toolbar-button:hover{background: var(--toolbar-hover,var(--bg-secondary,#e9ecef));color: var(--toolbar-icon,var(--text-primary,#333))}.overtype-toolbar-button:active{transform: scale(0.95)}.overtype-toolbar-button.active{background: var(--toolbar-active,var(--primary,#007bff));color: var(--toolbar-icon,var(--text-primary,#333))}.overtype-toolbar-button:disabled{opacity: 0.5;cursor: not-allowed}.overtype-toolbar-separator{width: 1px;height: 24px;background: var(--border,#e0e0e0);margin: 0 4px;flex-shrink: 0}@media (max-width: 640px){.overtype-toolbar{padding: 6px;gap: 2px}.overtype-toolbar-button{width: 36px;height: 36px}.overtype-toolbar-separator{margin: 0 2px}}.overtype-container[data-mode="plain"] .overtype-preview{display: none !important}.overtype-container[data-mode="plain"] .overtype-input{color: var(--text,#0d3b66) !important;font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif !important}.overtype-container:not([data-mode="plain"]) .overtype-input{color: transparent !important}.overtype-toolbar-button{position: relative !important}.overtype-toolbar-button.dropdown-active{background: var(--toolbar-active,var(--hover-bg,#f0f0f0))}.overtype-dropdown-menu{position: fixed !important;background: var(--bg-secondary,white) !important;border: 1px solid var(--border,#e0e0e0) !important;border-radius: 6px;box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;z-index: 10000;min-width: 150px;padding: 4px 0 !important}.overtype-dropdown-item{display: flex;align-items: center;width: 100%;padding: 8px 12px;border: none;background: none;text-align: left;cursor: pointer;font-size: 14px;color: var(--text,#333);font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}.overtype-dropdown-item:hover{background: var(--hover-bg,#f0f0f0)}.overtype-dropdown-item.active{font-weight: 600}.overtype-dropdown-check{width: 16px;margin-right: 8px;color: var(--h1,#007bff)}.overtype-dropdown-icon{width: 20px;margin-right: 8px;text-align: center}.overtype-container[data-mode="preview"] .overtype-input{display: none !important}.overtype-container[data-mode="preview"] .overtype-preview{pointer-events: auto !important;user-select: text !important;cursor: text !important}.overtype-container.overtype-auto-resize[data-mode="preview"] .overtype-preview{position: static !important;height: auto !important}.overtype-container[data-mode="preview"] .syntax-marker{display: none !important}.overtype-container[data-mode="preview"] .syntax-marker.url-part,.overtype-container[data-mode="preview"] .url-part{display: none !important}.overtype-container[data-mode="preview"] a .syntax-marker{display: none !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h1,.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h2,.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h3{font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif !important;font-weight: 600 !important;margin: 0 !important;display: block !important;line-height: 1 !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h1{font-size: 2em !important;color: var(--preview-h1,var(--preview-h1-default)) !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h2{font-size: 1.5em !important;color: var(--preview-h2,var(--preview-h2-default)) !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h3{font-size: 1.17em !important;color: var(--preview-h3,var(--preview-h3-default)) !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview ul{display: block !important;list-style: disc !important;padding-left: 2em !important;margin: 1em 0 !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview ol{display: block !important;list-style: decimal !important;padding-left: 2em !important;margin: 1em 0 !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview li{display: list-item !important;margin: 0 !important;padding: 0 !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview li.task-list{list-style: none !important;position: relative !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview li.task-list input[type="checkbox"]{margin-right: 0.5em !important;cursor: default !important;vertical-align: middle !important}.overtype-container:not([data-mode="preview"]) .overtype-wrapper .overtype-preview li.task-list{list-style: none !important}.overtype-container:not([data-mode="preview"]) .overtype-wrapper .overtype-preview li.task-list .syntax-marker{color: var(--syntax,#999999) !important;font-weight: normal !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview a{pointer-events: auto !important;cursor: pointer !important;color: var(--preview-link,var(--preview-link-default)) !important;text-decoration: underline !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block{background: var(--preview-code-bg,var(--preview-code-bg-default)) !important;color: var(--preview-code,var(--preview-code-default)) !important;padding: 1.2em !important;border-radius: 3px !important;overflow-x: auto !important;margin: 0 !important;display: block !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block code{background: transparent !important;color: inherit !important;padding: 0 !important;font-family:${o}!important;font-size: 0.9em !important;line-height: 1.4 !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .code-block-line{display: none !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .code-fence{display: none !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .blockquote{display: block !important;border-left: 4px solid var(--preview-blockquote,var(--preview-blockquote-default)) !important;color: var(--preview-blockquote,var(--preview-blockquote-default)) !important;padding-left: 1em !important;margin: 1em 0 !important;font-style: italic !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview{font-family: Georgia,'Times New Roman',serif !important;font-size: 16px !important;line-height: 1.8 !important;color: var(--preview-text,var(--preview-text-default)) !important;background: var(--preview-bg,var(--preview-bg-default)) !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview code{font-family:${o}!important;font-size: 0.9em !important;background: var(--preview-code-bg,var(--preview-code-bg-default)) !important;color: var(--preview-code,var(--preview-code-default)) !important;padding: 0.2em 0.4em !important;border-radius: 3px !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview strong{font-weight: 700 !important;color: var(--preview-strong,var(--preview-strong-default)) !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview em{font-style: italic !important;color: var(--preview-em,var(--preview-em-default)) !important}.overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .hr-marker{display: block !important;border-top: 2px solid var(--preview-hr,var(--preview-hr-default)) !important;text-indent: -9999px !important;height: 2px !important}.overtype-link-tooltip{background: #333 !important;color: white !important;padding: 6px 10px !important;border-radius: 16px !important;font-size: 12px !important;font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif !important;display: flex !important;visibility: hidden !important;pointer-events: none !important;z-index: 10000 !important;cursor: pointer !important;box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;max-width: 300px !important;white-space: nowrap !important;overflow: hidden !important;text-overflow: ellipsis !important;position: fixed;top: 0;left: 0}.overtype-link-tooltip.visible{visibility: visible !important;pointer-events: auto !important}${a} + `}var ne={};st(ne,{applyCustomFormat:()=>Et,default:()=>In,expandSelection:()=>St,getActiveFormats:()=>Ie,getDebugMode:()=>Ze,hasFormat:()=>Lt,insertHeader:()=>de,insertLink:()=>Ae,preserveSelection:()=>bt,setDebugMode:()=>gt,setUndoMethod:()=>yt,toggleBold:()=>Ee,toggleBulletList:()=>Me,toggleCode:()=>Te,toggleH1:()=>He,toggleH2:()=>Pe,toggleH3:()=>Oe,toggleItalic:()=>Ce,toggleNumberedList:()=>$e,toggleQuote:()=>_e,toggleTaskList:()=>pe});var Sn=Object.defineProperty,dt=Object.getOwnPropertySymbols,En=Object.prototype.hasOwnProperty,Cn=Object.prototype.propertyIsEnumerable,ut=(t,e,n)=>e in t?Sn(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,ht=(t,e)=>{for(var n in e||(e={}))En.call(e,n)&&ut(t,n,e[n]);if(dt)for(var n of dt(e))Cn.call(e,n)&&ut(t,n,e[n]);return t},I={bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](url)",replaceNext:"url",scanFor:"https?://"},bulletList:{prefix:"- ",multiline:!0,unorderedList:!0},numberedList:{prefix:"1. ",multiline:!0,orderedList:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},taskList:{prefix:"- [ ] ",multiline:!0,surroundWithNewlines:!0},header1:{prefix:"# "},header2:{prefix:"## "},header3:{prefix:"### "},header4:{prefix:"#### "},header5:{prefix:"##### "},header6:{prefix:"###### "}};function Tn(){return{prefix:"",suffix:"",blockPrefix:"",blockSuffix:"",multiline:!1,replaceNext:"",prefixSpace:!1,scanFor:"",surroundWithNewlines:!1,orderedList:!1,unorderedList:!1,trimFirst:!1}}function R(t){return ht(ht({},Tn()),t)}var ce=!1;function gt(t){ce=t}function Ze(){return ce}function x(t,e,n){ce&&(console.group(`\u{1F50D} ${t}`),console.log(e),n&&console.log("Data:",n),console.groupEnd())}function Le(t,e){if(!ce)return;let n=t.value.slice(t.selectionStart,t.selectionEnd);console.group(`\u{1F4CD} Selection: ${e}`),console.log("Position:",`${t.selectionStart}-${t.selectionEnd}`),console.log("Selected text:",JSON.stringify(n)),console.log("Length:",n.length);let o=t.value.slice(Math.max(0,t.selectionStart-10),t.selectionStart),i=t.value.slice(t.selectionEnd,Math.min(t.value.length,t.selectionEnd+10));console.log("Context:",JSON.stringify(o)+"[SELECTION]"+JSON.stringify(i)),console.groupEnd()}function vt(t){ce&&(console.group("\u{1F4DD} Result"),console.log("Text to insert:",JSON.stringify(t.text)),console.log("New selection:",`${t.selectionStart}-${t.selectionEnd}`),console.groupEnd())}var M=null;function N(t,{text:e,selectionStart:n,selectionEnd:o}){let i=Ze();i&&(console.group("\u{1F527} insertText"),console.log("Current selection:",`${t.selectionStart}-${t.selectionEnd}`),console.log("Text to insert:",JSON.stringify(e)),console.log("New selection to set:",n,"-",o)),t.focus();let r=t.selectionStart,s=t.selectionEnd,a=t.value.slice(0,r),l=t.value.slice(s);i&&(console.log("Before text (last 20):",JSON.stringify(a.slice(-20))),console.log("After text (first 20):",JSON.stringify(l.slice(0,20))),console.log("Selected text being replaced:",JSON.stringify(t.value.slice(r,s))));let p=t.value,c=r!==s;if(M===null||M===!0){t.contentEditable="true";try{M=document.execCommand("insertText",!1,e),i&&console.log("execCommand returned:",M,"for text with",e.split(` +`).length,"lines")}catch(d){M=!1,i&&console.log("execCommand threw error:",d)}t.contentEditable="false"}if(i&&(console.log("canInsertText before:",M),console.log("execCommand result:",M)),M){let d=a+e+l,u=t.value;i&&(console.log("Expected length:",d.length),console.log("Actual length:",u.length)),u!==d&&i&&(console.log("execCommand changed the value but not as expected"),console.log("Expected:",JSON.stringify(d.slice(0,100))),console.log("Actual:",JSON.stringify(u.slice(0,100))))}if(!M)if(i&&console.log("Using manual insertion"),t.value===p){i&&console.log("Value unchanged, doing manual replacement");try{document.execCommand("ms-beginUndoUnit")}catch(d){}t.value=a+e+l;try{document.execCommand("ms-endUndoUnit")}catch(d){}t.dispatchEvent(new CustomEvent("input",{bubbles:!0,cancelable:!0}))}else i&&console.log("Value was changed by execCommand, skipping manual insertion");i&&console.log("Setting selection range:",n,o),n!=null&&o!=null?t.setSelectionRange(n,o):t.setSelectionRange(r,t.selectionEnd),i&&(console.log("Final value length:",t.value.length),console.groupEnd())}function yt(t){switch(t){case"native":M=!0;break;case"manual":M=!1;break;case"auto":M=null;break}}function Ke(t){return t.trim().split(` +`).length>1}function An(t,e){let n=e;for(;t[n]&&t[n-1]!=null&&!t[n-1].match(/\s/);)n--;return n}function Mn(t,e,n){let o=e,i=n?/\n/:/\s/;for(;t[o]&&!t[o].match(i);)o++;return o}function wt(t){let e=t.value.split(` +`),n=0;for(let o=0;o=n&&t.selectionStart=n&&t.selectionEnd0&&s[a-1]!==` +`;)a--;if(r){let p=o;for(;p0?`${s} +`:i,y=Ke(m)&&a&&a.length>0?` +${a}`:r;if(p){let k=t.value[t.selectionStart-1];t.selectionStart!==0&&k!=null&&!k.match(/\s/)&&(g=` ${g}`)}m=$n(t,g,y,e.multiline);let w=t.selectionStart,b=t.selectionEnd,L=l&&l.length>0&&y.indexOf(l)>-1&&m.length>0;if(d){let k=Qe(t);n=k.newlinesToAppend,o=k.newlinesToPrepend,g=n+i,y+=o}if(m.startsWith(g)&&m.endsWith(y)){let k=m.slice(g.length,m.length-y.length);if(h===f){let E=h-g.length;E=Math.max(E,w),E=Math.min(E,w+k.length),w=b=E}else b=w+k.length;return{text:k,selectionStart:w,selectionEnd:b}}else if(L)if(c&&c.length>0&&m.match(c)){y=y.replace(l,m);let k=g+y;return w=b=w+g.length,{text:k,selectionStart:w,selectionEnd:b}}else{let k=g+m+y;return w=w+g.length+m.length+y.indexOf(l),b=w+l.length,{text:k,selectionStart:w,selectionEnd:b}}else{let k=g+m+y;w=h+g.length,b=f+g.length;let E=m.match(/^\s*|\s*$/g);if(u&&E){let be=E[0]||"",Y=E[1]||"";k=be+g+m.trim()+y+Y,w+=be.length,b-=Y.length}return{text:k,selectionStart:w,selectionEnd:b}}}function Je(t,e){let{prefix:n,suffix:o,surroundWithNewlines:i}=e,r=t.value.slice(t.selectionStart,t.selectionEnd),s=t.selectionStart,a=t.selectionEnd,l=r.split(` +`);if(l.every(c=>c.startsWith(n)&&(!o||c.endsWith(o))))r=l.map(c=>{let d=c.slice(n.length);return o&&(d=d.slice(0,d.length-o.length)),d}).join(` +`),a=s+r.length;else if(r=l.map(c=>n+c+(o||"")).join(` +`),i){let{newlinesToAppend:c,newlinesToPrepend:d}=Qe(t);s+=c.length,a=s+r.length,r=c+r+d}return{text:r,selectionStart:s,selectionEnd:a}}function ft(t){let e=t.split(` +`),n=/^\d+\.\s+/,o=e.every(r=>n.test(r)),i=e;return o&&(i=e.map(r=>r.replace(n,""))),{text:i.join(` +`),processed:o}}function mt(t){let e=t.split(` +`),n="- ",o=e.every(r=>r.startsWith(n)),i=e;return o&&(i=e.map(r=>r.slice(n.length))),{text:i.join(` +`),processed:o}}function le(t,e){return e?"- ":`${t+1}. `}function _n(t,e){let n,o,i;return t.orderedList?(n=ft(e),o=mt(n.text),i=o.text):(n=mt(e),o=ft(n.text),i=o.text),[n,o,i]}function Hn(t,e){let n=t.selectionStart===t.selectionEnd,o=t.selectionStart,i=t.selectionEnd;wt(t);let r=t.value.slice(t.selectionStart,t.selectionEnd),[s,a,l]=_n(e,r),p=l.split(` +`).map((m,g)=>`${le(g,e.unorderedList)}${m}`),c=p.reduce((m,g,y)=>m+le(y,e.unorderedList).length,0),d=p.reduce((m,g,y)=>m+le(y,!e.unorderedList).length,0);if(s.processed)return n?(o=Math.max(o-le(0,e.unorderedList).length,0),i=o):(o=t.selectionStart,i=t.selectionEnd-c),{text:l,selectionStart:o,selectionEnd:i};let{newlinesToAppend:u,newlinesToPrepend:h}=Qe(t),f=u+p.join(` +`)+h;return n?(o=Math.max(o+le(0,e.unorderedList).length+u.length,0),i=o):a.processed?(o=Math.max(t.selectionStart+u.length,0),i=t.selectionEnd+u.length+c-d):(o=Math.max(t.selectionStart+u.length,0),i=t.selectionEnd+u.length+c),{text:f,selectionStart:o,selectionEnd:i}}function xt(t,e){let n=Se(t,o=>Hn(o,e),{adjustSelection:(o,i,r,s)=>{let a=t.value.slice(s,t.selectionEnd),l=/^\d+\.\s+/,p=/^- /,c=l.test(a),d=p.test(a),u=e.orderedList&&c||e.unorderedList&&d;if(i===r)if(u){let h=a.match(e.orderedList?l:p),f=h?h[0].length:0;return{start:Math.max(i-f,s),end:Math.max(i-f,s)}}else if(c||d){let h=a.match(c?l:p),f=h?h[0].length:0,g=(e.unorderedList?2:3)-f;return{start:i+g,end:i+g}}else{let h=e.unorderedList?2:3;return{start:i+h,end:i+h}}else if(u){let h=a.match(e.orderedList?l:p),f=h?h[0].length:0;return{start:Math.max(i-f,s),end:Math.max(r-f,s)}}else if(c||d){let h=a.match(c?l:p),f=h?h[0].length:0,g=(e.unorderedList?2:3)-f;return{start:i+g,end:r+g}}else{let h=e.unorderedList?2:3;return{start:i+h,end:r+h}}}});N(t,n)}function kt(t){if(!t)return[];let e=[],{selectionStart:n,selectionEnd:o,value:i}=t,r=i.split(` +`),s=0,a="";for(let d of r){if(n>=s&&n<=s+d.length){a=d;break}s+=d.length+1}a.startsWith("- ")&&(a.startsWith("- [ ] ")||a.startsWith("- [x] ")?e.push("task-list"):e.push("bullet-list")),/^\d+\.\s/.test(a)&&e.push("numbered-list"),a.startsWith("> ")&&e.push("quote"),a.startsWith("# ")&&e.push("header"),a.startsWith("## ")&&e.push("header-2"),a.startsWith("### ")&&e.push("header-3");let l=Math.max(0,n-10),p=Math.min(i.length,o+10),c=i.slice(l,p);if(c.includes("**")){let d=i.slice(Math.max(0,n-100),n),u=i.slice(o,Math.min(i.length,o+100)),h=d.lastIndexOf("**"),f=u.indexOf("**");h!==-1&&f!==-1&&e.push("bold")}if(c.includes("_")){let d=i.slice(Math.max(0,n-100),n),u=i.slice(o,Math.min(i.length,o+100)),h=d.lastIndexOf("_"),f=u.indexOf("_");h!==-1&&f!==-1&&e.push("italic")}if(c.includes("`")){let d=i.slice(Math.max(0,n-100),n),u=i.slice(o,Math.min(i.length,o+100));d.includes("`")&&u.includes("`")&&e.push("code")}if(c.includes("[")&&c.includes("]")){let d=i.slice(Math.max(0,n-100),n),u=i.slice(o,Math.min(i.length,o+100)),h=d.lastIndexOf("["),f=u.indexOf("]");h!==-1&&f!==-1&&i.slice(o+f+1,o+f+10).startsWith("(")&&e.push("link")}return e}function Pn(t,e){return kt(t).includes(e)}function On(t,e={}){if(!t)return;let{toWord:n,toLine:o,toFormat:i}=e,{selectionStart:r,selectionEnd:s,value:a}=t;if(o){let l=a.split(` +`),p=0,c=0,d=0;for(let u of l){if(r>=d&&r<=d+u.length){p=d,c=d+u.length;break}d+=u.length+1}t.selectionStart=p,t.selectionEnd=c}else if(n&&r===s){let l=r,p=s;for(;l>0&&!/\s/.test(a[l-1]);)l--;for(;pJe(o,e),{prefix:e.prefix});vt(n),N(t,n),Le(t,"Final")}function pe(t){if(!t||t.disabled||t.readOnly)return;let e=R(I.taskList),n=Se(t,o=>Je(o,e),{prefix:e.prefix});N(t,n)}function de(t,e=1,n=!1){if(!t||t.disabled||t.readOnly)return;(e<1||e>6)&&(e=1),x("insertHeader","============ START ============"),x("insertHeader",`Level: ${e}, Toggle: ${n}`),x("insertHeader",`Initial cursor: ${t.selectionStart}-${t.selectionEnd}`);let o=`header${e===1?"1":e}`,i=R(I[o]||I.header1);x("insertHeader",`Style prefix: "${i.prefix}"`);let r=t.value,s=t.selectionStart,a=t.selectionEnd,l=s;for(;l>0&&r[l-1]!==` +`;)l--;let p=a;for(;p{let y=g.value.slice(g.selectionStart,g.selectionEnd);x("insertHeader",`Line in operation: "${y}"`);let w=y.replace(/^#{1,6}\s*/,"");x("insertHeader",`Cleaned line: "${w}"`);let b;return f?(x("insertHeader","ACTION: Toggling OFF - removing header"),b=w):u>0?(x("insertHeader",`ACTION: Replacing H${u} with H${e}`),b=i.prefix+w):(x("insertHeader","ACTION: Adding new header"),b=i.prefix+w),x("insertHeader",`New line: "${b}"`),{text:b,selectionStart:g.selectionStart,selectionEnd:g.selectionEnd}},{prefix:i.prefix,adjustSelection:(g,y,w,b)=>{if(x("insertHeader","Adjusting selection:"),x("insertHeader",` - isRemoving param: ${g}`),x("insertHeader",` - shouldToggleOff: ${f}`),x("insertHeader",` - selStart: ${y}, selEnd: ${w}`),x("insertHeader",` - lineStartPos: ${b}`),f){let L=Math.max(y-h,b);return x("insertHeader",` - Removing header, adjusting by -${h}`),{start:L,end:y===w?L:Math.max(w-h,b)}}else if(h>0){let L=i.prefix.length-h;return x("insertHeader",` - Replacing header, adjusting by ${L}`),{start:y+L,end:w+L}}else return x("insertHeader",` - Adding header, adjusting by +${i.prefix.length}`),{start:y+i.prefix.length,end:w+i.prefix.length}}});x("insertHeader",`Final result: text="${m.text}", cursor=${m.selectionStart}-${m.selectionEnd}`),x("insertHeader","============ END ============"),N(t,m)}function He(t){de(t,1,!0)}function Pe(t){de(t,2,!0)}function Oe(t){de(t,3,!0)}function Ie(t){return kt(t)}function Lt(t,e){return Pn(t,e)}function St(t,e={}){On(t,e)}function Et(t,e){if(!t||t.disabled||t.readOnly)return;let n=R(e),o;if(n.multiline){let i=t.value.slice(t.selectionStart,t.selectionEnd);Ke(i)?o=Je(t,n):o=te(t,n)}else o=te(t,n);N(t,o)}var In={toggleBold:Ee,toggleItalic:Ce,toggleCode:Te,insertLink:Ae,toggleBulletList:Me,toggleNumberedList:$e,toggleQuote:_e,toggleTaskList:pe,insertHeader:de,toggleH1:He,toggleH2:Pe,toggleH3:Oe,getActiveFormats:Ie,hasFormat:Lt,expandSelection:St,applyCustomFormat:Et,preserveSelection:bt,setUndoMethod:yt,setDebugMode:gt,getDebugMode:Ze};var Re=class{constructor(e,n={}){this.editor=e,this.container=null,this.buttons={},this.toolbarButtons=n.toolbarButtons||[]}create(){this.container=document.createElement("div"),this.container.className="overtype-toolbar",this.container.setAttribute("role","toolbar"),this.container.setAttribute("aria-label","Formatting toolbar"),this.toolbarButtons.forEach(e=>{if(e.name==="separator"){let n=this.createSeparator();this.container.appendChild(n)}else{let n=this.createButton(e);this.buttons[e.name]=n,this.container.appendChild(n)}}),this.editor.container.insertBefore(this.container,this.editor.wrapper)}createSeparator(){let e=document.createElement("div");return e.className="overtype-toolbar-separator",e.setAttribute("role","separator"),e}createButton(e){let n=document.createElement("button");return n.className="overtype-toolbar-button",n.type="button",n.setAttribute("data-button",e.name),n.title=e.title||"",n.setAttribute("aria-label",e.title||e.name),n.innerHTML=this.sanitizeSVG(e.icon||""),e.name==="viewMode"?(n.classList.add("has-dropdown"),n.dataset.dropdown="true",n.addEventListener("click",o=>{o.preventDefault(),this.toggleViewModeDropdown(n)}),n):(n._clickHandler=o=>{o.preventDefault();let i=e.actionId||e.name;this.editor.performAction(i,o)},n.addEventListener("click",n._clickHandler),n)}async handleAction(e){if(e&&typeof e=="object"&&typeof e.action=="function"){this.editor.textarea.focus();try{return await e.action({editor:this.editor,getValue:()=>this.editor.getValue(),setValue:n=>this.editor.setValue(n),event:null}),!0}catch(n){return console.error(`Action "${e.name}" error:`,n),this.editor.wrapper.dispatchEvent(new CustomEvent("button-error",{detail:{buttonName:e.name,error:n}})),!1}}return typeof e=="string"?this.editor.performAction(e,null):!1}sanitizeSVG(e){return typeof e!="string"?"":e.replace(/)<[^<]*)*<\/script>/gi,"").replace(/\son\w+\s*=\s*["'][^"']*["']/gi,"").replace(/\son\w+\s*=\s*[^\s>]*/gi,"")}toggleViewModeDropdown(e){let n=document.querySelector(".overtype-dropdown-menu");if(n){n.remove(),e.classList.remove("dropdown-active");return}e.classList.add("dropdown-active");let o=this.createViewModeDropdown(e),i=e.getBoundingClientRect();o.style.position="absolute",o.style.top=`${i.bottom+5}px`,o.style.left=`${i.left}px`,document.body.appendChild(o),this.handleDocumentClick=r=>{!o.contains(r.target)&&!e.contains(r.target)&&(o.remove(),e.classList.remove("dropdown-active"),document.removeEventListener("click",this.handleDocumentClick))},setTimeout(()=>{document.addEventListener("click",this.handleDocumentClick)},0)}createViewModeDropdown(e){let n=document.createElement("div");n.className="overtype-dropdown-menu";let o=[{id:"normal",label:"Normal Edit",icon:"\u2713"},{id:"plain",label:"Plain Textarea",icon:"\u2713"},{id:"preview",label:"Preview Mode",icon:"\u2713"}],i=this.editor.container.dataset.mode||"normal";return o.forEach(r=>{let s=document.createElement("button");if(s.className="overtype-dropdown-item",s.type="button",s.textContent=r.label,r.id===i){s.classList.add("active"),s.setAttribute("aria-current","true");let a=document.createElement("span");a.className="overtype-dropdown-icon",a.textContent=r.icon,s.prepend(a)}s.addEventListener("click",a=>{switch(a.preventDefault(),r.id){case"plain":this.editor.showPlainTextarea();break;case"preview":this.editor.showPreviewMode();break;case"normal":default:this.editor.showNormalEditMode();break}n.remove(),e.classList.remove("dropdown-active"),document.removeEventListener("click",this.handleDocumentClick)}),n.appendChild(s)}),n}updateButtonStates(){var e;try{let n=((e=Ie)==null?void 0:e(this.editor.textarea,this.editor.textarea.selectionStart))||[];Object.entries(this.buttons).forEach(([o,i])=>{if(o==="viewMode")return;let r=!1;switch(o){case"bold":r=n.includes("bold");break;case"italic":r=n.includes("italic");break;case"code":r=!1;break;case"bulletList":r=n.includes("bullet-list");break;case"orderedList":r=n.includes("numbered-list");break;case"taskList":r=n.includes("task-list");break;case"quote":r=n.includes("quote");break;case"h1":r=n.includes("header");break;case"h2":r=n.includes("header-2");break;case"h3":r=n.includes("header-3");break}i.classList.toggle("active",r),i.setAttribute("aria-pressed",r.toString())})}catch(n){}}show(){this.container&&this.container.classList.remove("overtype-toolbar-hidden")}hide(){this.container&&this.container.classList.add("overtype-toolbar-hidden")}destroy(){this.container&&(this.handleDocumentClick&&document.removeEventListener("click",this.handleDocumentClick),Object.values(this.buttons).forEach(e=>{e._clickHandler&&(e.removeEventListener("click",e._clickHandler),delete e._clickHandler)}),this.container.remove(),this.container=null,this.buttons={})}};var he=Math.min,W=Math.max,fe=Math.round;var P=t=>({x:t,y:t}),Rn={left:"right",right:"left",bottom:"top",top:"bottom"},Bn={start:"end",end:"start"};function Xe(t,e,n){return W(t,he(e,n))}function me(t,e){return typeof t=="function"?t(e):t}function q(t){return t.split("-")[0]}function ge(t){return t.split("-")[1]}function Ge(t){return t==="x"?"y":"x"}function Ye(t){return t==="y"?"height":"width"}var Fn=new Set(["top","bottom"]);function D(t){return Fn.has(q(t))?"y":"x"}function et(t){return Ge(D(t))}function At(t,e,n){n===void 0&&(n=!1);let o=ge(t),i=et(t),r=Ye(i),s=i==="x"?o===(n?"end":"start")?"right":"left":o==="start"?"bottom":"top";return e.reference[r]>e.floating[r]&&(s=ue(s)),[s,ue(s)]}function Mt(t){let e=ue(t);return[Be(t),e,Be(e)]}function Be(t){return t.replace(/start|end/g,e=>Bn[e])}var Ct=["left","right"],Tt=["right","left"],Nn=["top","bottom"],Dn=["bottom","top"];function zn(t,e,n){switch(t){case"top":case"bottom":return n?e?Tt:Ct:e?Ct:Tt;case"left":case"right":return e?Nn:Dn;default:return[]}}function $t(t,e,n,o){let i=ge(t),r=zn(q(t),n==="start",o);return i&&(r=r.map(s=>s+"-"+i),e&&(r=r.concat(r.map(Be)))),r}function ue(t){return t.replace(/left|right|bottom|top/g,e=>Rn[e])}function jn(t){return{top:0,right:0,bottom:0,left:0,...t}}function _t(t){return typeof t!="number"?jn(t):{top:t,right:t,bottom:t,left:t}}function J(t){let{x:e,y:n,width:o,height:i}=t;return{width:o,height:i,top:n,left:e,right:e+o,bottom:n+i,x:e,y:n}}function Ht(t,e,n){let{reference:o,floating:i}=t,r=D(e),s=et(e),a=Ye(s),l=q(e),p=r==="y",c=o.x+o.width/2-i.width/2,d=o.y+o.height/2-i.height/2,u=o[a]/2-i[a]/2,h;switch(l){case"top":h={x:c,y:o.y-i.height};break;case"bottom":h={x:c,y:o.y+o.height};break;case"right":h={x:o.x+o.width,y:d};break;case"left":h={x:o.x-i.width,y:d};break;default:h={x:o.x,y:o.y}}switch(ge(e)){case"start":h[s]-=u*(n&&p?-1:1);break;case"end":h[s]+=u*(n&&p?-1:1);break}return h}async function Pt(t,e){var n;e===void 0&&(e={});let{x:o,y:i,platform:r,rects:s,elements:a,strategy:l}=t,{boundary:p="clippingAncestors",rootBoundary:c="viewport",elementContext:d="floating",altBoundary:u=!1,padding:h=0}=me(e,t),f=_t(h),g=a[u?d==="floating"?"reference":"floating":d],y=J(await r.getClippingRect({element:(n=await(r.isElement==null?void 0:r.isElement(g)))==null||n?g:g.contextElement||await(r.getDocumentElement==null?void 0:r.getDocumentElement(a.floating)),boundary:p,rootBoundary:c,strategy:l})),w=d==="floating"?{x:o,y:i,width:s.floating.width,height:s.floating.height}:s.reference,b=await(r.getOffsetParent==null?void 0:r.getOffsetParent(a.floating)),L=await(r.isElement==null?void 0:r.isElement(b))?await(r.getScale==null?void 0:r.getScale(b))||{x:1,y:1}:{x:1,y:1},k=J(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:a,rect:w,offsetParent:b,strategy:l}):w);return{top:(y.top-k.top+f.top)/L.y,bottom:(k.bottom-y.bottom+f.bottom)/L.y,left:(y.left-k.left+f.left)/L.x,right:(k.right-y.right+f.right)/L.x}}var Ot=async(t,e,n)=>{let{placement:o="bottom",strategy:i="absolute",middleware:r=[],platform:s}=n,a=r.filter(Boolean),l=await(s.isRTL==null?void 0:s.isRTL(e)),p=await s.getElementRects({reference:t,floating:e,strategy:i}),{x:c,y:d}=Ht(p,o,l),u=o,h={},f=0;for(let g=0;gK<=0)){var ot,it;let K=(((ot=r.flip)==null?void 0:ot.index)||0)+1,We=be[K];if(We&&(!(d==="alignment"?w!==D(We):!1)||ee.every(H=>D(H.placement)===w?H.overflows[0]>0:!0)))return{data:{index:K,overflows:ee},reset:{placement:We}};let se=(it=ee.filter(Z=>Z.overflows[0]<=0).sort((Z,H)=>Z.overflows[1]-H.overflows[1])[0])==null?void 0:it.placement;if(!se)switch(h){case"bestFit":{var rt;let Z=(rt=ee.filter(H=>{if(E){let V=D(H.placement);return V===w||V==="y"}return!0}).map(H=>[H.placement,H.overflows.filter(V=>V>0).reduce((V,vn)=>V+vn,0)]).sort((H,V)=>H[1]-V[1])[0])==null?void 0:rt[0];Z&&(se=Z);break}case"initialPlacement":se=a;break}if(i!==se)return{reset:{placement:se}}}return{}}}};var Vn=new Set(["left","top"]);async function Un(t,e){let{placement:n,platform:o,elements:i}=t,r=await(o.isRTL==null?void 0:o.isRTL(i.floating)),s=q(n),a=ge(n),l=D(n)==="y",p=Vn.has(s)?-1:1,c=r&&l?-1:1,d=me(e,t),{mainAxis:u,crossAxis:h,alignmentAxis:f}=typeof d=="number"?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return a&&typeof f=="number"&&(h=a==="end"?f*-1:f),l?{x:h*c,y:u*p}:{x:u*p,y:h*c}}var Rt=function(t){return t===void 0&&(t=0),{name:"offset",options:t,async fn(e){var n,o;let{x:i,y:r,placement:s,middlewareData:a}=e,l=await Un(e,t);return s===((n=a.offset)==null?void 0:n.placement)&&(o=a.arrow)!=null&&o.alignmentOffset?{}:{x:i+l.x,y:r+l.y,data:{...l,placement:s}}}}},Bt=function(t){return t===void 0&&(t={}),{name:"shift",options:t,async fn(e){let{x:n,y:o,placement:i,platform:r}=e,{mainAxis:s=!0,crossAxis:a=!1,limiter:l={fn:y=>{let{x:w,y:b}=y;return{x:w,y:b}}},...p}=me(t,e),c={x:n,y:o},d=await r.detectOverflow(e,p),u=D(q(i)),h=Ge(u),f=c[h],m=c[u];if(s){let y=h==="y"?"top":"left",w=h==="y"?"bottom":"right",b=f+d[y],L=f-d[w];f=Xe(b,f,L)}if(a){let y=u==="y"?"top":"left",w=u==="y"?"bottom":"right",b=m+d[y],L=m-d[w];m=Xe(b,m,L)}let g=l.fn({...e,[h]:f,[u]:m});return{...g,data:{x:g.x-n,y:g.y-o,enabled:{[h]:s,[u]:a}}}}}};function Ne(){return typeof window<"u"}function X(t){return Nt(t)?(t.nodeName||"").toLowerCase():"#document"}function A(t){var e;return(t==null||(e=t.ownerDocument)==null?void 0:e.defaultView)||window}function B(t){var e;return(e=(Nt(t)?t.ownerDocument:t.document)||window.document)==null?void 0:e.documentElement}function Nt(t){return Ne()?t instanceof Node||t instanceof A(t).Node:!1}function $(t){return Ne()?t instanceof Element||t instanceof A(t).Element:!1}function O(t){return Ne()?t instanceof HTMLElement||t instanceof A(t).HTMLElement:!1}function Ft(t){return!Ne()||typeof ShadowRoot>"u"?!1:t instanceof ShadowRoot||t instanceof A(t).ShadowRoot}var Wn=new Set(["inline","contents"]);function oe(t){let{overflow:e,overflowX:n,overflowY:o,display:i}=_(t);return/auto|scroll|overlay|hidden|clip/.test(e+o+n)&&!Wn.has(i)}var qn=new Set(["table","td","th"]);function Dt(t){return qn.has(X(t))}var Kn=[":popover-open",":modal"];function ve(t){return Kn.some(e=>{try{return t.matches(e)}catch(n){return!1}})}var Zn=["transform","translate","scale","rotate","perspective"],Qn=["transform","translate","scale","rotate","perspective","filter"],Jn=["paint","layout","strict","content"];function De(t){let e=ze(),n=$(t)?_(t):t;return Zn.some(o=>n[o]?n[o]!=="none":!1)||(n.containerType?n.containerType!=="normal":!1)||!e&&(n.backdropFilter?n.backdropFilter!=="none":!1)||!e&&(n.filter?n.filter!=="none":!1)||Qn.some(o=>(n.willChange||"").includes(o))||Jn.some(o=>(n.contain||"").includes(o))}function zt(t){let e=z(t);for(;O(e)&&!G(e);){if(De(e))return e;if(ve(e))return null;e=z(e)}return null}function ze(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}var Xn=new Set(["html","body","#document"]);function G(t){return Xn.has(X(t))}function _(t){return A(t).getComputedStyle(t)}function ye(t){return $(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.scrollX,scrollTop:t.scrollY}}function z(t){if(X(t)==="html")return t;let e=t.assignedSlot||t.parentNode||Ft(t)&&t.host||B(t);return Ft(e)?e.host:e}function jt(t){let e=z(t);return G(e)?t.ownerDocument?t.ownerDocument.body:t.body:O(e)&&oe(e)?e:jt(e)}function Fe(t,e,n){var o;e===void 0&&(e=[]),n===void 0&&(n=!0);let i=jt(t),r=i===((o=t.ownerDocument)==null?void 0:o.body),s=A(i);if(r){let a=je(s);return e.concat(s,s.visualViewport||[],oe(i)?i:[],a&&n?Fe(a):[])}return e.concat(i,Fe(i,[],n))}function je(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function qt(t){let e=_(t),n=parseFloat(e.width)||0,o=parseFloat(e.height)||0,i=O(t),r=i?t.offsetWidth:n,s=i?t.offsetHeight:o,a=fe(n)!==r||fe(o)!==s;return a&&(n=r,o=s),{width:n,height:o,$:a}}function Kt(t){return $(t)?t:t.contextElement}function ie(t){let e=Kt(t);if(!O(e))return P(1);let n=e.getBoundingClientRect(),{width:o,height:i,$:r}=qt(e),s=(r?fe(n.width):n.width)/o,a=(r?fe(n.height):n.height)/i;return(!s||!Number.isFinite(s))&&(s=1),(!a||!Number.isFinite(a))&&(a=1),{x:s,y:a}}var Gn=P(0);function Zt(t){let e=A(t);return!ze()||!e.visualViewport?Gn:{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}}function Yn(t,e,n){return e===void 0&&(e=!1),!n||e&&n!==A(t)?!1:e}function we(t,e,n,o){e===void 0&&(e=!1),n===void 0&&(n=!1);let i=t.getBoundingClientRect(),r=Kt(t),s=P(1);e&&(o?$(o)&&(s=ie(o)):s=ie(t));let a=Yn(r,n,o)?Zt(r):P(0),l=(i.left+a.x)/s.x,p=(i.top+a.y)/s.y,c=i.width/s.x,d=i.height/s.y;if(r){let u=A(r),h=o&&$(o)?A(o):o,f=u,m=je(f);for(;m&&o&&h!==f;){let g=ie(m),y=m.getBoundingClientRect(),w=_(m),b=y.left+(m.clientLeft+parseFloat(w.paddingLeft))*g.x,L=y.top+(m.clientTop+parseFloat(w.paddingTop))*g.y;l*=g.x,p*=g.y,c*=g.x,d*=g.y,l+=b,p+=L,f=A(m),m=je(f)}}return J({width:c,height:d,x:l,y:p})}function Ve(t,e){let n=ye(t).scrollLeft;return e?e.left+n:we(B(t)).left+n}function Qt(t,e){let n=t.getBoundingClientRect(),o=n.left+e.scrollLeft-Ve(t,n),i=n.top+e.scrollTop;return{x:o,y:i}}function eo(t){let{elements:e,rect:n,offsetParent:o,strategy:i}=t,r=i==="fixed",s=B(o),a=e?ve(e.floating):!1;if(o===s||a&&r)return n;let l={scrollLeft:0,scrollTop:0},p=P(1),c=P(0),d=O(o);if((d||!d&&!r)&&((X(o)!=="body"||oe(s))&&(l=ye(o)),O(o))){let h=we(o);p=ie(o),c.x=h.x+o.clientLeft,c.y=h.y+o.clientTop}let u=s&&!d&&!r?Qt(s,l):P(0);return{width:n.width*p.x,height:n.height*p.y,x:n.x*p.x-l.scrollLeft*p.x+c.x+u.x,y:n.y*p.y-l.scrollTop*p.y+c.y+u.y}}function to(t){return Array.from(t.getClientRects())}function no(t){let e=B(t),n=ye(t),o=t.ownerDocument.body,i=W(e.scrollWidth,e.clientWidth,o.scrollWidth,o.clientWidth),r=W(e.scrollHeight,e.clientHeight,o.scrollHeight,o.clientHeight),s=-n.scrollLeft+Ve(t),a=-n.scrollTop;return _(o).direction==="rtl"&&(s+=W(e.clientWidth,o.clientWidth)-i),{width:i,height:r,x:s,y:a}}var Vt=25;function oo(t,e){let n=A(t),o=B(t),i=n.visualViewport,r=o.clientWidth,s=o.clientHeight,a=0,l=0;if(i){r=i.width,s=i.height;let c=ze();(!c||c&&e==="fixed")&&(a=i.offsetLeft,l=i.offsetTop)}let p=Ve(o);if(p<=0){let c=o.ownerDocument,d=c.body,u=getComputedStyle(d),h=c.compatMode==="CSS1Compat"&&parseFloat(u.marginLeft)+parseFloat(u.marginRight)||0,f=Math.abs(o.clientWidth-d.clientWidth-h);f<=Vt&&(r-=f)}else p<=Vt&&(r+=p);return{width:r,height:s,x:a,y:l}}var io=new Set(["absolute","fixed"]);function ro(t,e){let n=we(t,!0,e==="fixed"),o=n.top+t.clientTop,i=n.left+t.clientLeft,r=O(t)?ie(t):P(1),s=t.clientWidth*r.x,a=t.clientHeight*r.y,l=i*r.x,p=o*r.y;return{width:s,height:a,x:l,y:p}}function Ut(t,e,n){let o;if(e==="viewport")o=oo(t,n);else if(e==="document")o=no(B(t));else if($(e))o=ro(e,n);else{let i=Zt(t);o={x:e.x-i.x,y:e.y-i.y,width:e.width,height:e.height}}return J(o)}function Jt(t,e){let n=z(t);return n===e||!$(n)||G(n)?!1:_(n).position==="fixed"||Jt(n,e)}function so(t,e){let n=e.get(t);if(n)return n;let o=Fe(t,[],!1).filter(a=>$(a)&&X(a)!=="body"),i=null,r=_(t).position==="fixed",s=r?z(t):t;for(;$(s)&&!G(s);){let a=_(s),l=De(s);!l&&a.position==="fixed"&&(i=null),(r?!l&&!i:!l&&a.position==="static"&&!!i&&io.has(i.position)||oe(s)&&!l&&Jt(t,s))?o=o.filter(c=>c!==s):i=a,s=z(s)}return e.set(t,o),o}function ao(t){let{element:e,boundary:n,rootBoundary:o,strategy:i}=t,s=[...n==="clippingAncestors"?ve(e)?[]:so(e,this._c):[].concat(n),o],a=s[0],l=s.reduce((p,c)=>{let d=Ut(e,c,i);return p.top=W(d.top,p.top),p.right=he(d.right,p.right),p.bottom=he(d.bottom,p.bottom),p.left=W(d.left,p.left),p},Ut(e,a,i));return{width:l.right-l.left,height:l.bottom-l.top,x:l.left,y:l.top}}function lo(t){let{width:e,height:n}=qt(t);return{width:e,height:n}}function co(t,e,n){let o=O(e),i=B(e),r=n==="fixed",s=we(t,!0,r,e),a={scrollLeft:0,scrollTop:0},l=P(0);function p(){l.x=Ve(i)}if(o||!o&&!r)if((X(e)!=="body"||oe(i))&&(a=ye(e)),o){let h=we(e,!0,r,e);l.x=h.x+e.clientLeft,l.y=h.y+e.clientTop}else i&&p();r&&!o&&i&&p();let c=i&&!o&&!r?Qt(i,a):P(0),d=s.left+a.scrollLeft-l.x-c.x,u=s.top+a.scrollTop-l.y-c.y;return{x:d,y:u,width:s.width,height:s.height}}function tt(t){return _(t).position==="static"}function Wt(t,e){if(!O(t)||_(t).position==="fixed")return null;if(e)return e(t);let n=t.offsetParent;return B(t)===n&&(n=n.ownerDocument.body),n}function Xt(t,e){let n=A(t);if(ve(t))return n;if(!O(t)){let i=z(t);for(;i&&!G(i);){if($(i)&&!tt(i))return i;i=z(i)}return n}let o=Wt(t,e);for(;o&&Dt(o)&&tt(o);)o=Wt(o,e);return o&&G(o)&&tt(o)&&!De(o)?n:o||zt(t)||n}var po=async function(t){let e=this.getOffsetParent||Xt,n=this.getDimensions,o=await n(t.floating);return{reference:co(t.reference,await e(t.floating),t.strategy),floating:{x:0,y:0,width:o.width,height:o.height}}};function uo(t){return _(t).direction==="rtl"}var ho={convertOffsetParentRelativeRectToViewportRelativeRect:eo,getDocumentElement:B,getClippingRect:ao,getOffsetParent:Xt,getElementRects:po,getClientRects:to,getDimensions:lo,getScale:ie,isElement:$,isRTL:uo};var Gt=Rt;var Yt=Bt,en=It;var tn=(t,e,n)=>{let o=new Map,i={platform:ho,...n},r={...i.platform,_c:o};return Ot(t,e,{...i,platform:r})};var Ue=class{constructor(e){this.editor=e,this.tooltip=null,this.currentLink=null,this.hideTimeout=null,this.visibilityChangeHandler=null,this.isTooltipHovered=!1,this.init()}init(){this.createTooltip(),this.editor.textarea.addEventListener("selectionchange",()=>this.checkCursorPosition()),this.editor.textarea.addEventListener("keyup",e=>{(e.key.includes("Arrow")||e.key==="Home"||e.key==="End")&&this.checkCursorPosition()}),this.editor.textarea.addEventListener("input",()=>this.hide()),this.editor.textarea.addEventListener("scroll",()=>{this.currentLink&&this.positionTooltip(this.currentLink)}),this.editor.textarea.addEventListener("blur",()=>{this.isTooltipHovered||this.hide()}),this.visibilityChangeHandler=()=>{document.hidden&&this.hide()},document.addEventListener("visibilitychange",this.visibilityChangeHandler),this.tooltip.addEventListener("mouseenter",()=>{this.isTooltipHovered=!0,this.cancelHide()}),this.tooltip.addEventListener("mouseleave",()=>{this.isTooltipHovered=!1,this.scheduleHide()})}createTooltip(){this.tooltip=document.createElement("div"),this.tooltip.className="overtype-link-tooltip",this.tooltip.innerHTML=` + + + + + + + + `,this.tooltip.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation(),this.currentLink&&(window.open(this.currentLink.url,"_blank"),this.hide())}),this.editor.container.appendChild(this.tooltip)}checkCursorPosition(){let e=this.editor.textarea.selectionStart,n=this.editor.textarea.value,o=this.findLinkAtPosition(n,e);o?(!this.currentLink||this.currentLink.url!==o.url||this.currentLink.index!==o.index)&&this.show(o):this.scheduleHide()}findLinkAtPosition(e,n){let o=/\[([^\]]+)\]\(([^)]+)\)/g,i,r=0;for(;(i=o.exec(e))!==null;){let s=i.index,a=i.index+i[0].length;if(n>=s&&n<=a)return{text:i[1],url:i[2],index:r,start:s,end:a};r++}return null}async show(e){this.currentLink=e,this.cancelHide();let n=this.tooltip.querySelector(".overtype-link-tooltip-url");n.textContent=e.url,await this.positionTooltip(e),this.currentLink===e&&this.tooltip.classList.add("visible")}async positionTooltip(e){let n=this.findAnchorElement(e.index);if(!n)return;let o=n.getBoundingClientRect();if(!(o.width===0||o.height===0))try{let{x:i,y:r}=await tn(n,this.tooltip,{strategy:"fixed",placement:"bottom",middleware:[Gt(8),Yt({padding:8}),en()]});Object.assign(this.tooltip.style,{left:`${i}px`,top:`${r}px`,position:"fixed"})}catch(i){console.warn("Floating UI positioning failed:",i)}}findAnchorElement(e){return this.editor.preview.querySelector(`a[style*="--link-${e}"]`)}hide(){this.tooltip.classList.remove("visible"),this.currentLink=null,this.isTooltipHovered=!1}scheduleHide(){this.cancelHide(),this.hideTimeout=setTimeout(()=>this.hide(),300)}cancelHide(){this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=null)}destroy(){this.cancelHide(),this.visibilityChangeHandler&&(document.removeEventListener("visibilitychange",this.visibilityChangeHandler),this.visibilityChangeHandler=null),this.tooltip&&this.tooltip.parentNode&&this.tooltip.parentNode.removeChild(this.tooltip),this.tooltip=null,this.currentLink=null,this.isTooltipHovered=!1}};var nn='',on='',rn='',sn='',an='',ln='',cn='',pn='',dn='',un='',hn='',fn='',mn='';var S={bold:{name:"bold",actionId:"toggleBold",icon:nn,title:"Bold (Ctrl+B)",action:({editor:t})=>{Ee(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},italic:{name:"italic",actionId:"toggleItalic",icon:on,title:"Italic (Ctrl+I)",action:({editor:t})=>{Ce(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},code:{name:"code",actionId:"toggleCode",icon:cn,title:"Inline Code",action:({editor:t})=>{Te(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},separator:{name:"separator"},link:{name:"link",actionId:"insertLink",icon:ln,title:"Insert Link",action:({editor:t})=>{Ae(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},h1:{name:"h1",actionId:"toggleH1",icon:rn,title:"Heading 1",action:({editor:t})=>{He(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},h2:{name:"h2",actionId:"toggleH2",icon:sn,title:"Heading 2",action:({editor:t})=>{Pe(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},h3:{name:"h3",actionId:"toggleH3",icon:an,title:"Heading 3",action:({editor:t})=>{Oe(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},bulletList:{name:"bulletList",actionId:"toggleBulletList",icon:pn,title:"Bullet List",action:({editor:t})=>{Me(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},orderedList:{name:"orderedList",actionId:"toggleNumberedList",icon:dn,title:"Numbered List",action:({editor:t})=>{$e(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},taskList:{name:"taskList",actionId:"toggleTaskList",icon:hn,title:"Task List",action:({editor:t})=>{pe&&(pe(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0})))}},quote:{name:"quote",actionId:"toggleQuote",icon:un,title:"Quote",action:({editor:t})=>{_e(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},upload:{name:"upload",actionId:"uploadFile",icon:fn,title:"Upload File",action:({editor:t})=>{var n,o;if(!((n=t.options.fileUpload)!=null&&n.enabled))return;let e=document.createElement("input");e.type="file",e.multiple=!0,((o=t.options.fileUpload.mimeTypes)==null?void 0:o.length)>0&&(e.accept=t.options.fileUpload.mimeTypes.join(",")),e.onchange=()=>{var r;if(!((r=e.files)!=null&&r.length))return;let i=new DataTransfer;for(let s of e.files)i.items.add(s);t._handleDataTransfer(i)},e.click()}},viewMode:{name:"viewMode",icon:mn,title:"View mode"}},re=[S.bold,S.italic,S.code,S.separator,S.link,S.separator,S.h1,S.h2,S.h3,S.separator,S.bulletList,S.orderedList,S.taskList,S.separator,S.quote,S.separator,S.viewMode];function nt(t){let e={};return(t||[]).forEach(n=>{if(!n||n.name==="separator")return;let o=n.actionId||n.name;n.action&&(e[o]=n.action)}),e}function gn(t){let e=t||re;return Array.isArray(e)?e.map(n=>({name:(n==null?void 0:n.name)||null,actionId:(n==null?void 0:n.actionId)||(n==null?void 0:n.name)||null,icon:(n==null?void 0:n.icon)||null,title:(n==null?void 0:n.title)||null})):null}function mo(t,e){let n=gn(t),o=gn(e);if(n===null||o===null)return n!==o;if(n.length!==o.length)return!0;for(let i=0;i{if(r.overTypeInstance)return r.overTypeInstance.reinit(n),r.overTypeInstance;let s=Object.create(v.prototype);return s._init(r,n),r.overTypeInstance=s,v.instances.set(r,s),s})}_init(e,n={}){this.element=e,this.instanceTheme=n.theme||null,this.options=this._mergeOptions(n),this.instanceId=++v.instanceCount,this.initialized=!1,v.injectStyles(),v.initGlobalListeners();let o=e.querySelector(".overtype-container"),i=e.querySelector(".overtype-wrapper");o||i?this._recoverFromDOM(o,i):this._buildFromScratch(),this.instanceTheme==="auto"&&this.setTheme("auto"),this.shortcuts=new ae(this),this._rebuildActionsMap(),this.linkTooltip=new Ue(this),requestAnimationFrame(()=>{requestAnimationFrame(()=>{this.textarea.scrollTop=this.preview.scrollTop,this.textarea.scrollLeft=this.preview.scrollLeft})}),this.initialized=!0,this.options.onChange&&this._notifyChange()}_mergeOptions(e){let n={fontSize:"14px",lineHeight:1.6,fontFamily:'"SF Mono", SFMono-Regular, Menlo, Monaco, "Cascadia Code", Consolas, "Roboto Mono", "Noto Sans Mono", "Droid Sans Mono", "Ubuntu Mono", "DejaVu Sans Mono", "Liberation Mono", "Courier New", Courier, monospace',padding:"16px",mobile:{fontSize:"16px",padding:"12px",lineHeight:1.5},textareaProps:{},autofocus:!1,autoResize:!1,minHeight:"100px",maxHeight:null,placeholder:"Start typing...",value:"",onChange:null,onKeydown:null,onRender:null,onFocus:null,onBlur:null,showActiveLineRaw:!1,showStats:!1,toolbar:!1,toolbarButtons:null,statsFormatter:null,smartLists:!0,codeHighlighter:null,spellcheck:!1},{theme:o,colors:i,...r}=e;return{...n,...r}}_recoverFromDOM(e,n){if(e&&e.classList.contains("overtype-container"))this.container=e,this.wrapper=e.querySelector(".overtype-wrapper");else if(n){this.wrapper=n,this.container=document.createElement("div"),this.container.className="overtype-container";let o=this.instanceTheme||v.currentTheme||F,i=typeof o=="string"?o:o.name;if(i&&this.container.setAttribute("data-theme",i),this.instanceTheme){let r=typeof this.instanceTheme=="string"?U(this.instanceTheme):this.instanceTheme;if(r&&r.colors){let s=Q(r.colors);this.container.style.cssText+=s}}n.parentNode.insertBefore(this.container,n),this.container.appendChild(n)}if(!this.wrapper){e&&e.remove(),n&&n.remove(),this._buildFromScratch();return}if(this.textarea=this.wrapper.querySelector(".overtype-input"),this.preview=this.wrapper.querySelector(".overtype-preview"),!this.textarea||!this.preview){this.container.remove(),this._buildFromScratch();return}this.wrapper._instance=this,this._applyInstanceCSSVars(),this._configureTextarea(),this._applyOptions()}_buildFromScratch(){let e=this._extractContent();this.element.innerHTML="",this._createDOM(),(e||this.options.value)&&this.setValue(e||this.options.value),this._applyOptions()}_extractContent(){let e=this.element.querySelector(".overtype-input");return e?e.value:this.element.textContent||""}_createDOM(){this.container=document.createElement("div"),this.container.className="overtype-container";let e=this.instanceTheme||v.currentTheme||F,n=typeof e=="string"?e:e.name;if(n&&this.container.setAttribute("data-theme",n),this.instanceTheme){let o=typeof this.instanceTheme=="string"?U(this.instanceTheme):this.instanceTheme;if(o&&o.colors){let i=Q(o.colors);this.container.style.cssText+=i}}this.wrapper=document.createElement("div"),this.wrapper.className="overtype-wrapper",this._applyInstanceCSSVars(),this.wrapper._instance=this,this.textarea=document.createElement("textarea"),this.textarea.className="overtype-input",this.textarea.placeholder=this.options.placeholder,this._configureTextarea(),this.options.textareaProps&&Object.entries(this.options.textareaProps).forEach(([o,i])=>{o==="className"||o==="class"?this.textarea.className+=" "+i:o==="style"&&typeof i=="object"?Object.assign(this.textarea.style,i):this.textarea.setAttribute(o,i)}),this.preview=document.createElement("div"),this.preview.className="overtype-preview",this.preview.setAttribute("aria-hidden","true"),this.placeholderEl=document.createElement("div"),this.placeholderEl.className="overtype-placeholder",this.placeholderEl.setAttribute("aria-hidden","true"),this.placeholderEl.textContent=this.options.placeholder,this.wrapper.appendChild(this.textarea),this.wrapper.appendChild(this.preview),this.wrapper.appendChild(this.placeholderEl),this.container.appendChild(this.wrapper),this.options.showStats&&(this.statsBar=document.createElement("div"),this.statsBar.className="overtype-stats",this.container.appendChild(this.statsBar),this._updateStats()),this.element.appendChild(this.container),this.options.autoResize?this._setupAutoResize():this.container.classList.remove("overtype-auto-resize")}_configureTextarea(){this.textarea.setAttribute("autocomplete","off"),this.textarea.setAttribute("autocorrect","off"),this.textarea.setAttribute("autocapitalize","off"),this.textarea.setAttribute("spellcheck",String(this.options.spellcheck)),this.textarea.setAttribute("data-gramm","false"),this.textarea.setAttribute("data-gramm_editor","false"),this.textarea.setAttribute("data-enable-grammarly","false")}_createToolbar(){var n;let e=this.options.toolbarButtons||re;if((n=this.options.fileUpload)!=null&&n.enabled&&!e.some(o=>(o==null?void 0:o.name)==="upload")){let o=e.findIndex(i=>(i==null?void 0:i.name)==="viewMode");o!==-1?(e=[...e],e.splice(o,0,S.separator,S.upload)):e=[...e,S.separator,S.upload]}this.toolbar=new Re(this,{toolbarButtons:e}),this.toolbar.create(),this._toolbarSelectionListener=()=>{this.toolbar&&this.toolbar.updateButtonStates()},this._toolbarInputListener=()=>{this.toolbar&&this.toolbar.updateButtonStates()},this.textarea.addEventListener("selectionchange",this._toolbarSelectionListener),this.textarea.addEventListener("input",this._toolbarInputListener)}_cleanupToolbarListeners(){this._toolbarSelectionListener&&(this.textarea.removeEventListener("selectionchange",this._toolbarSelectionListener),this._toolbarSelectionListener=null),this._toolbarInputListener&&(this.textarea.removeEventListener("input",this._toolbarInputListener),this._toolbarInputListener=null)}_rebuildActionsMap(){var e;this.actionsById=nt(re),this.options.toolbarButtons&&Object.assign(this.actionsById,nt(this.options.toolbarButtons)),(e=this.options.fileUpload)!=null&&e.enabled&&Object.assign(this.actionsById,nt([S.upload]))}_applyInstanceCSSVars(){this.wrapper&&(this.options.fontSize&&this.wrapper.style.setProperty("--instance-font-size",this.options.fontSize),this.options.lineHeight&&this.wrapper.style.setProperty("--instance-line-height",String(this.options.lineHeight)),this.options.padding&&this.wrapper.style.setProperty("--instance-padding",this.options.padding),this.options.fontFamily&&this.wrapper.style.setProperty("--instance-font-family",this.options.fontFamily))}_applyOptions(){this._applyInstanceCSSVars(),this.options.autofocus&&this.textarea.focus(),this.options.autoResize?this.container.classList.contains("overtype-auto-resize")?this._updateAutoHeight():this._setupAutoResize():this.container.classList.remove("overtype-auto-resize"),this.options.toolbar&&!this.toolbar?this._createToolbar():!this.options.toolbar&&this.toolbar&&(this._cleanupToolbarListeners(),this.toolbar.destroy(),this.toolbar=null),this.placeholderEl&&(this.placeholderEl.textContent=this.options.placeholder),this.options.fileUpload&&!this.fileUploadInitialized?this._initFileUpload():!this.options.fileUpload&&this.fileUploadInitialized&&this._destroyFileUpload(),this.updatePreview()}_initFileUpload(){let e=this.options.fileUpload;if(!(!e||!e.enabled)){if(e.maxSize=e.maxSize||10*1024*1024,e.mimeTypes=e.mimeTypes||[],e.batch=e.batch||!1,!e.onInsertFile||typeof e.onInsertFile!="function"){console.warn("OverType: fileUpload.onInsertFile callback is required for file uploads.");return}this._fileUploadCounter=0,this._uploadedFiles=new Map,this._boundHandleFilePaste=this._handleFilePaste.bind(this),this._boundHandleFileDrop=this._handleFileDrop.bind(this),this._boundHandleDragOver=this._handleDragOver.bind(this),this.textarea.addEventListener("paste",this._boundHandleFilePaste),this.textarea.addEventListener("drop",this._boundHandleFileDrop),this.textarea.addEventListener("dragover",this._boundHandleDragOver),this.fileUploadInitialized=!0}}_extractMarkdownUrls(e){let n=[],o=/!?\[[^\]]*\]\(([^)\s]+)/g,i;for(;(i=o.exec(e))!==null;)n.push(i[1]);return n}_trackInsertedUrls(e,n){if(!(!this._uploadedFiles||!n||!e))for(let o of this._extractMarkdownUrls(e))this._uploadedFiles.set(o,{filename:n.name,file:n})}_checkForRemovedUploads(){var i;if(!this._uploadedFiles||this._uploadedFiles.size===0)return;let e=(i=this.options.fileUpload)==null?void 0:i.onRemoveFile,n=this.textarea.value,o=[];for(let[r,s]of this._uploadedFiles)n.includes(r)||o.push({url:r,info:s});for(let{url:r,info:s}of o)this._uploadedFiles.delete(r),e&&e({url:r,filename:s.filename,file:s.file})}_handleFilePaste(e){var n,o;(o=(n=e==null?void 0:e.clipboardData)==null?void 0:n.files)!=null&&o.length&&(e.preventDefault(),this._handleDataTransfer(e.clipboardData))}_handleFileDrop(e){var n,o;(o=(n=e==null?void 0:e.dataTransfer)==null?void 0:n.files)!=null&&o.length&&(e.preventDefault(),this._handleDataTransfer(e.dataTransfer))}_handleDataTransfer(e){let n=[];for(let o of e.files){if(o.size>this.options.fileUpload.maxSize||this.options.fileUpload.mimeTypes.length>0&&!this.options.fileUpload.mimeTypes.includes(o.type))continue;let i=++this._fileUploadCounter,s=`${o.type.startsWith("image/")?"!":""}[Uploading ${o.name} (#${i})...]()`;if(this.insertAtCursor(`${s} +`),this.options.fileUpload.batch){n.push({file:o,placeholder:s});continue}this.options.fileUpload.onInsertFile(o).then(a=>{this.textarea.value=this.textarea.value.replace(s,a),this._trackInsertedUrls(a,o),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))},a=>{console.error("OverType: File upload failed",a),this.textarea.value=this.textarea.value.replace(s,"[Upload failed]()"),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))})}this.options.fileUpload.batch&&n.length>0&&this.options.fileUpload.onInsertFile(n.map(o=>o.file)).then(o=>{(Array.isArray(o)?o:[o]).forEach((r,s)=>{this.textarea.value=this.textarea.value.replace(n[s].placeholder,r),this._trackInsertedUrls(r,n[s].file)}),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))},o=>{console.error("OverType: File upload failed",o),n.forEach(({placeholder:i})=>{this.textarea.value=this.textarea.value.replace(i,"[Upload failed]()")}),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))})}_handleDragOver(e){e.preventDefault()}_destroyFileUpload(){this.textarea.removeEventListener("paste",this._boundHandleFilePaste),this.textarea.removeEventListener("drop",this._boundHandleFileDrop),this.textarea.removeEventListener("dragover",this._boundHandleDragOver),this._boundHandleFilePaste=null,this._boundHandleFileDrop=null,this._boundHandleDragOver=null,this._uploadedFiles=null,this.fileUploadInitialized=!1}insertAtCursor(e){let n=this.textarea.selectionStart,o=this.textarea.selectionEnd,i=!1;try{i=document.execCommand("insertText",!1,e)}catch(r){}if(!i){let r=this.textarea.value.slice(0,n),s=this.textarea.value.slice(o);this.textarea.value=r+e+s,this.textarea.setSelectionRange(n+e.length,n+e.length)}this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}updatePreview(){let e=this.textarea.value,n=this.textarea.selectionStart,o=this._getCurrentLine(e,n),i=this.container.dataset.mode==="preview",r=C.parse(e,o,this.options.showActiveLineRaw,this.options.codeHighlighter,i);this.preview.innerHTML=r,this.placeholderEl&&(this.placeholderEl.style.display=e?"none":""),this._applyCodeBlockBackgrounds(),this.options.showStats&&this.statsBar&&this._updateStats(),this.options.onRender&&this.options.onRender(this.preview,i?"preview":"normal",this)}_notifyChange(){this.initialized&&(this._checkForRemovedUploads(),this.options.onChange&&this.options.onChange(this.textarea.value,this))}_applyCodeBlockBackgrounds(){let e=this.preview.querySelectorAll(".code-fence");for(let n=0;nd.replace(/^ /,"")).join(` +`);document.execCommand?(this.textarea.setSelectionRange(o,i),document.execCommand("insertText",!1,c)):(this.textarea.value=s+c+l,this.textarea.selectionStart=o,this.textarea.selectionEnd=o+c.length)}else if(o!==i){let s=r.substring(0,o),a=r.substring(o,i),l=r.substring(i),c=a.split(` +`).map(d=>" "+d).join(` +`);document.execCommand?(this.textarea.setSelectionRange(o,i),document.execCommand("insertText",!1,c)):(this.textarea.value=s+c+l,this.textarea.selectionStart=o,this.textarea.selectionEnd=o+c.length)}else document.execCommand?document.execCommand("insertText",!1," "):(this.textarea.value=r.substring(0,o)+" "+r.substring(i),this.textarea.selectionStart=this.textarea.selectionEnd=o+2);this.textarea.dispatchEvent(new Event("input",{bubbles:!0}));return}if(e.key==="Enter"&&!e.shiftKey&&!e.metaKey&&!e.ctrlKey&&this.options.smartLists&&this.handleSmartListContinuation()){e.preventDefault();return}!this.shortcuts.handleKeydown(e)&&this.options.onKeydown&&this.options.onKeydown(e,this)}handleSmartListContinuation(){let e=this.textarea,n=e.selectionStart,o=C.getListContext(e.value,n);return!o||!o.inList?!1:o.content.trim()===""&&n>=o.markerEndPos?(this.deleteListMarker(o),!0):(n>o.markerEndPos&&n{this.updateNumberedLists()},10)}updateNumberedLists(){let e=this.textarea.value,n=this.textarea.selectionStart,o=C.renumberLists(e);if(o!==e){let i=0,r=e.split(` +`),s=o.split(` +`),a=0;for(let p=0;pthis.getValue(),setValue:s=>this.setValue(s),event:n}),!0}catch(s){return console.error(`OverType: Action "${e}" error:`,s),this.wrapper.dispatchEvent(new CustomEvent("button-error",{detail:{actionId:e,error:s}})),!1}}getRenderedHTML(e={}){let n=this.getValue(),o=C.parse(n,-1,!1,this.options.codeHighlighter);return e.cleanHTML&&(o=o.replace(/.*?<\/span>/g,""),o=o.replace(/\sclass="(bullet-list|ordered-list|code-fence|hr-marker|blockquote|url-part)"/g,""),o=o.replace(/\sclass=""/g,"")),o}getPreviewHTML(){return this.preview.innerHTML}getCleanHTML(){return this.getRenderedHTML({cleanHTML:!0})}focus(){this.textarea.focus()}blur(){this.textarea.blur()}isInitialized(){return this.initialized}reinit(e={}){var i;let n=(i=this.options)==null?void 0:i.toolbarButtons;this.options=this._mergeOptions({...this.options,...e});let o=this.toolbar&&this.options.toolbar&&mo(n,this.options.toolbarButtons);this._rebuildActionsMap(),o&&(this._cleanupToolbarListeners(),this.toolbar.destroy(),this.toolbar=null,this._createToolbar()),this.fileUploadInitialized&&this._destroyFileUpload(),this.options.fileUpload&&this._initFileUpload(),this._applyOptions(),this.updatePreview()}showToolbar(){this.toolbar?this.toolbar.show():this._createToolbar()}hideToolbar(){this.toolbar&&this.toolbar.hide()}setTheme(e){if(v._autoInstances.delete(this),this.instanceTheme=e,e==="auto")v._autoInstances.add(this),v._startAutoListener(),this._applyResolvedTheme(qe("auto"));else{let n=typeof e=="string"?U(e):e,o=typeof n=="string"?n:n.name;if(o&&this.container.setAttribute("data-theme",o),n&&n.colors){let i=Q(n.colors,n.previewColors);this.container.style.cssText+=i}this.updatePreview()}return v._stopAutoListener(),this}_applyResolvedTheme(e){let n=U(e);this.container.setAttribute("data-theme",e),n&&n.colors&&(this.container.style.cssText=Q(n.colors,n.previewColors)),this.updatePreview()}setCodeHighlighter(e){this.options.codeHighlighter=e,this.updatePreview()}_updateStats(){if(!this.statsBar)return;let e=this.textarea.value,n=e.split(` +`),o=e.length,i=e.split(/\s+/).filter(c=>c.length>0).length,r=this.textarea.selectionStart,a=e.substring(0,r).split(` +`),l=a.length,p=a[a.length-1].length+1;this.options.statsFormatter?this.statsBar.innerHTML=this.options.statsFormatter({chars:o,words:i,lines:n.length,line:l,column:p}):this.statsBar.innerHTML=` +
    + + ${o} chars, ${i} words, ${n.length} lines +
    +
    Line ${l}, Col ${p}
    + `}_setupAutoResize(){this.container.classList.add("overtype-auto-resize"),this.previousHeight=null,this._updateAutoHeight(),this.textarea.addEventListener("input",()=>this._updateAutoHeight()),window.addEventListener("resize",()=>this._updateAutoHeight())}_updateAutoHeight(){if(!this.options.autoResize)return;let e=this.textarea,n=this.preview,o=this.wrapper;if(this.container.dataset.mode==="preview"){o.style.removeProperty("height"),n.style.removeProperty("height"),n.style.removeProperty("overflow-y"),e.style.removeProperty("height"),e.style.removeProperty("overflow-y");return}let r=e.scrollTop;o.style.setProperty("height","auto","important"),e.style.setProperty("height","auto","important");let s=e.scrollHeight;if(this.options.minHeight){let p=parseInt(this.options.minHeight);s=Math.max(s,p)}let a="hidden";if(this.options.maxHeight){let p=parseInt(this.options.maxHeight);s>p&&(s=p,a="auto")}let l=s+"px";e.style.setProperty("height",l,"important"),e.style.setProperty("overflow-y",a,"important"),n.style.setProperty("height",l,"important"),n.style.setProperty("overflow-y",a,"important"),o.style.setProperty("height",l,"important"),e.scrollTop=r,n.scrollTop=r,this.previousHeight!==s&&(this.previousHeight=s)}showStats(e){this.options.showStats=e,e&&!this.statsBar?(this.statsBar=document.createElement("div"),this.statsBar.className="overtype-stats",this.container.appendChild(this.statsBar),this._updateStats()):e&&this.statsBar?this._updateStats():!e&&this.statsBar&&(this.statsBar.remove(),this.statsBar=null)}showNormalEditMode(){return this.container.dataset.mode="normal",this.updatePreview(),this._updateAutoHeight(),requestAnimationFrame(()=>{this.textarea.scrollTop=this.preview.scrollTop,this.textarea.scrollLeft=this.preview.scrollLeft}),this}showPlainTextarea(){if(this.container.dataset.mode="plain",this._updateAutoHeight(),this.toolbar){let e=this.container.querySelector('[data-action="toggle-plain"]');e&&(e.classList.remove("active"),e.title="Show markdown preview")}return this}showPreviewMode(){return this.container.dataset.mode="preview",this.updatePreview(),this._updateAutoHeight(),this}destroy(){if(v._autoInstances.delete(this),v._stopAutoListener(),this.fileUploadInitialized&&this._destroyFileUpload(),this.element.overTypeInstance=null,v.instances.delete(this.element),this.shortcuts&&this.shortcuts.destroy(),this.wrapper){let e=this.getValue();this.wrapper.remove(),this.element.textContent=e}this.initialized=!1}static init(e,n={}){return new v(e,n)}static initFromData(e,n={}){return v._resolveTargets(e).map(i=>{let r={...n};for(let s of i.attributes)if(s.name.startsWith("data-ot-")){let l=s.name.slice(8).replace(/-([a-z])/g,(p,c)=>c.toUpperCase());r[l]=v._parseDataValue(s.value)}return new v(i,r)[0]})}static _resolveTargets(e){if(e==null)throw new Error("Invalid target: must be selector string, Element, NodeList, or Array");if(typeof e=="string")return Array.from(document.querySelectorAll(e));if(e instanceof Element)return[e];if(e instanceof NodeList)return Array.from(e);if(Array.isArray(e))return e;if(typeof e.length=="number")return Array.from(e);throw new Error("Invalid target: must be selector string, Element, NodeList, or Array")}static _parseDataValue(e){return e==="true"?!0:e==="false"?!1:e==="null"?null:e!==""&&!isNaN(Number(e))?Number(e):e}static getInstance(e){let n;return e instanceof Element?n=e:n=v._resolveTargets(e)[0],n&&(n.overTypeInstance||v.instances.get(n))||null}static destroyAll(){document.querySelectorAll("[data-overtype-instance]").forEach(n=>{let o=v.getInstance(n);o&&o.destroy()})}static injectStyles(e=!1){if(v.stylesInjected&&!e)return;let n=document.querySelector("style.overtype-styles");n&&n.remove();let o=v.currentTheme||F,i=pt({theme:o}),r=document.createElement("style");r.className="overtype-styles",r.textContent=i,document.head.appendChild(r),v.stylesInjected=!0}static setTheme(e,n=null){if(v._globalAutoTheme=!1,v._globalAutoCustomColors=null,e==="auto"){v._globalAutoTheme=!0,v._globalAutoCustomColors=n,v._startAutoListener(),v._applyGlobalTheme(qe("auto"),n);return}v._stopAutoListener(),v._applyGlobalTheme(e,n)}static _applyGlobalTheme(e,n=null){let o=typeof e=="string"?U(e):e;n&&(o=ct(o,n)),v.currentTheme=o,v.injectStyles(!0);let i=typeof o=="string"?o:o.name;document.querySelectorAll(".overtype-container").forEach(r=>{i&&r.setAttribute("data-theme",i)}),document.querySelectorAll(".overtype-wrapper").forEach(r=>{r.closest(".overtype-container")||i&&r.setAttribute("data-theme",i);let s=r._instance;s&&s.updatePreview()}),document.querySelectorAll("overtype-editor").forEach(r=>{i&&typeof r.setAttribute=="function"&&r.setAttribute("theme",i),typeof r.refreshTheme=="function"&&r.refreshTheme()})}static _startAutoListener(){v._autoMediaQuery||window.matchMedia&&(v._autoMediaQuery=window.matchMedia("(prefers-color-scheme: dark)"),v._autoMediaListener=e=>{let n=e.matches?"cave":"solar";v._globalAutoTheme&&v._applyGlobalTheme(n,v._globalAutoCustomColors),v._autoInstances.forEach(o=>o._applyResolvedTheme(n))},v._autoMediaQuery.addEventListener("change",v._autoMediaListener))}static _stopAutoListener(){v._autoInstances.size>0||v._globalAutoTheme||v._autoMediaQuery&&(v._autoMediaQuery.removeEventListener("change",v._autoMediaListener),v._autoMediaQuery=null,v._autoMediaListener=null)}static setCodeHighlighter(e){C.setCodeHighlighter(e),document.querySelectorAll(".overtype-wrapper").forEach(n=>{let o=n._instance;o&&o.updatePreview&&o.updatePreview()}),document.querySelectorAll("overtype-editor").forEach(n=>{if(typeof n.getEditor=="function"){let o=n.getEditor();o&&o.updatePreview&&o.updatePreview()}})}static setCustomSyntax(e){C.setCustomSyntax(e),document.querySelectorAll(".overtype-wrapper").forEach(n=>{let o=n._instance;o&&o.updatePreview&&o.updatePreview()}),document.querySelectorAll("overtype-editor").forEach(n=>{if(typeof n.getEditor=="function"){let o=n.getEditor();o&&o.updatePreview&&o.updatePreview()}})}static initGlobalListeners(){v.globalListenersInitialized||(document.addEventListener("input",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),o=n==null?void 0:n._instance;o&&o.handleInput(e)}}),document.addEventListener("keydown",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),o=n==null?void 0:n._instance;o&&o.handleKeydown(e)}}),document.addEventListener("focus",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),o=n==null?void 0:n._instance;o&&o.handleFocus(e)}},!0),document.addEventListener("blur",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),o=n==null?void 0:n._instance;o&&o.handleBlur(e)}},!0),document.addEventListener("scroll",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),o=n==null?void 0:n._instance;o&&o.handleScroll(e)}},!0),document.addEventListener("selectionchange",e=>{let n=document.activeElement;if(n&&n.classList.contains("overtype-input")){let o=n.closest(".overtype-wrapper"),i=o==null?void 0:o._instance;i&&(i.options.showStats&&i.statsBar&&i._updateStats(),clearTimeout(i._selectionTimeout),i._selectionTimeout=setTimeout(()=>{i.updatePreview()},50))}}),v.globalListenersInitialized=!0)}};T(v,"instances",new WeakMap),T(v,"stylesInjected",!1),T(v,"globalListenersInitialized",!1),T(v,"instanceCount",0),T(v,"_autoMediaQuery",null),T(v,"_autoMediaListener",null),T(v,"_autoInstances",new Set),T(v,"_globalAutoTheme",!1),T(v,"_globalAutoCustomColors",null);var j=v;j.MarkdownParser=C;j.ShortcutsManager=ae;j.themes={solar:F,cave:U("cave")};j.getTheme=U;j.currentTheme=F;var go=j;return Ln(vo);})(); +/** + * OverType - A lightweight markdown editor library with perfect WYSIWYG alignment + * @version 1.0.0 + * @license MIT + */ + +if (typeof window !== "undefined" && typeof window.document !== "undefined") { + // Extract exports BEFORE reassigning OverType (var OverType is window.OverType) + window.toolbarButtons = OverType.toolbarButtons; + window.defaultToolbarButtons = OverType.defaultToolbarButtons; + window.markdownActions = OverType.markdownActions; + window.OverType = OverType.default ? OverType.default : OverType; +} + diff --git a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/lib/overtype/2.3.4/overtype.min.js b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/lib/overtype/2.3.4/overtype.min.js deleted file mode 100644 index f4d8dcd3db..0000000000 --- a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/lib/overtype/2.3.4/overtype.min.js +++ /dev/null @@ -1,995 +0,0 @@ -/** - * OverType v2.3.4 - * A lightweight markdown editor library with perfect WYSIWYG alignment - * @license MIT - * @author David Miranda - * https://github.com/panphora/overtype - */ -var OverType=(()=>{var ve=Object.defineProperty;var cn=Object.getOwnPropertyDescriptor;var pn=Object.getOwnPropertyNames;var dn=Object.prototype.hasOwnProperty;var un=(t,e,n)=>e in t?ve(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n;var hn=(t,e)=>{for(var n in e)ve(t,n,{get:e[n],enumerable:!0})},fn=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of pn(e))!dn.call(t,o)&&o!==n&&ve(t,o,{get:()=>e[o],enumerable:!(i=cn(e,o))||i.enumerable});return t};var mn=t=>fn(ve({},"__esModule",{value:!0}),t);var C=(t,e,n)=>(un(t,typeof e!="symbol"?e+"":e,n),n);var ai={};hn(ai,{OverType:()=>z,default:()=>si,defaultToolbarButtons:()=>ie,toolbarButtons:()=>S});var T=class{static resetLinkIndex(){this.linkIndex=0}static setCodeHighlighter(e){this.codeHighlighter=e}static setCustomSyntax(e){this.customSyntax=e}static applyCustomSyntax(e){return this.customSyntax?this.customSyntax(e):e}static escapeHtml(e){let n={"&":"&","<":"<",">":">",'"':""","'":"'"};return e.replace(/[&<>"']/g,i=>n[i])}static preserveIndentation(e,n){let o=n.match(/^(\s*)/)[1].replace(/ /g," ");return e.replace(/^\s*/,o)}static parseHeader(e){return e.replace(/^(#{1,3})\s(.+)$/,(n,i,o)=>{let r=i.length;return o=this.parseInlineElements(o),`${i} ${o}`})}static parseHorizontalRule(e){return e.match(/^(-{3,}|\*{3,}|_{3,})$/)?`
    ${e}
    `:null}static parseBlockquote(e){return e.replace(/^> (.+)$/,(n,i)=>`> ${i}`)}static parseBulletList(e){return e.replace(/^((?: )*)([-*+])\s(.+)$/,(n,i,o,r)=>(r=this.parseInlineElements(r),`${i}
  • ${o} ${r}
  • `))}static parseTaskList(e,n=!1){return e.replace(/^((?: )*)-\s+\[([ xX])\]\s+(.+)$/,(i,o,r,s)=>{if(s=this.parseInlineElements(s),n){let a=r.toLowerCase()==="x";return`${o}
  • ${s}
  • `}else return`${o}
  • - [${r}] ${s}
  • `})}static parseNumberedList(e){return e.replace(/^((?: )*)(\d+\.)\s(.+)$/,(n,i,o,r)=>(r=this.parseInlineElements(r),`${i}
  • ${o} ${r}
  • `))}static parseCodeBlock(e){return/^`{3}[^`]*$/.test(e)?`
    ${e}
    `:null}static parseBold(e){return e=e.replace(/\*\*(.+?)\*\*/g,'**$1**'),e=e.replace(/__(.+?)__/g,'__$1__'),e}static parseItalic(e){return e=e.replace(new RegExp("(?])\\*(?!\\*)(.+?)(?*$1*'),e=e.replace(new RegExp("(?<=^|\\s)_(?!_)(.+?)(?_$1_'),e}static parseStrikethrough(e){return e=e.replace(new RegExp("(?~~$1~~'),e=e.replace(new RegExp("(?~$1~'),e}static parseInlineCode(e){return e.replace(new RegExp("(?$1$2$3')}static sanitizeUrl(e){let n=e.trim(),i=n.toLowerCase(),r=["http://","https://","mailto:","ftp://","ftps://"].some(a=>i.startsWith(a)),s=n.startsWith("/")||n.startsWith("#")||n.startsWith("?")||n.startsWith(".")||!n.includes(":")&&!n.includes("//");return r||s?e:"#"}static parseLinks(e){return e.replace(/\[(.+?)\]\((.+?)\)/g,(n,i,o)=>{let r=`--link-${this.linkIndex++}`;return`[${i}](${o})`})}static identifyAndProtectSanctuaries(e){let n=new Map,i=0,o=e,r=[],s=/\[([^\]]+)\]\(([^)]+)\)/g,a;for(;(a=s.exec(e))!==null;){let h=a.index+a[0].indexOf("](")+2,u=h+a[2].length;r.push({start:h,end:u})}let c=new RegExp("(?d>=f.start&&h<=f.end)||l.push({match:p[0],index:p.index,openTicks:p[1],content:p[2],closeTicks:p[3]})}return l.sort((d,h)=>h.index-d.index),l.forEach(d=>{let h=`\uE000${i++}\uE001`;n.set(h,{type:"code",original:d.match,openTicks:d.openTicks,content:d.content,closeTicks:d.closeTicks}),o=o.substring(0,d.index)+h+o.substring(d.index+d.match.length)}),o=o.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(d,h,u)=>{let f=`\uE000${i++}\uE001`;return n.set(f,{type:"link",original:d,linkText:h,url:u}),f}),{protectedText:o,sanctuaries:n}}static restoreAndTransformSanctuaries(e,n){return Array.from(n.keys()).sort((o,r)=>{let s=e.indexOf(o),a=e.indexOf(r);return s-a}).forEach(o=>{let r=n.get(o),s;if(r.type==="code")s=`${r.openTicks}${r.content}${r.closeTicks}`;else if(r.type==="link"){let a=r.linkText;n.forEach((l,d)=>{if(a.includes(d)&&l.type==="code"){let h=`${l.openTicks}${l.content}${l.closeTicks}`;a=a.replace(d,h)}}),a=this.parseStrikethrough(a),a=this.parseBold(a),a=this.parseItalic(a);let c=`--link-${this.linkIndex++}`;s=`[${a}](${r.url})`}e=e.replace(o,s)}),e}static parseInlineElements(e){let{protectedText:n,sanctuaries:i}=this.identifyAndProtectSanctuaries(e),o=n;return o=this.parseStrikethrough(o),o=this.parseBold(o),o=this.parseItalic(o),o=this.restoreAndTransformSanctuaries(o,i),o}static parseLine(e,n=!1){let i=this.escapeHtml(e);i=this.preserveIndentation(i,e);let o=this.parseHorizontalRule(i);if(o)return o;let r=this.parseCodeBlock(i);return r||(i=this.parseHeader(i),i=this.parseBlockquote(i),i=this.parseTaskList(i,n),i=this.parseBulletList(i),i=this.parseNumberedList(i),!i.includes(" 
    ":`
    ${i}
    `)}static parse(e,n=-1,i=!1,o,r=!1){this.resetLinkIndex();let s=e.split(` -`),a=!1,p=s.map((l,d)=>{if(i&&d===n)return`
    ${this.escapeHtml(l)||" "}
    `;if(/^```[^`]*$/.test(l))return a=!a,this.applyCustomSyntax(this.parseLine(l,r));if(a){let u=this.escapeHtml(l);return`
    ${this.preserveIndentation(u,l)||" "}
    `}return this.applyCustomSyntax(this.parseLine(l,r))}).join("");return this.postProcessHTML(p,o)}static postProcessHTML(e,n){if(typeof document>"u"||!document)return this.postProcessHTMLManual(e,n);let i=document.createElement("div");i.innerHTML=e;let o=null,r=null,s=null,a=!1,c=Array.from(i.children);for(let p=0;p0&&(s._codeContent+=` -`);let f=l.textContent.replace(/\u00A0/g," ");s._codeContent+=f,u.textContent.length>0&&(u.textContent+=` -`),u.textContent+=f,l.remove();continue}let h=null;if(l.tagName==="DIV"&&(h=l.querySelector("li")),h){let u=h.classList.contains("bullet-list"),f=h.classList.contains("ordered-list");if(!u&&!f){o=null,r=null;continue}let m=u?"ul":"ol";(!o||r!==m)&&(o=document.createElement(m),i.insertBefore(o,l),r=m);let g=[];for(let y of l.childNodes)if(y.nodeType===3&&y.textContent.match(/^\u00A0+$/))g.push(y.cloneNode(!0));else if(y===h)break;g.forEach(y=>{h.insertBefore(y,h.firstChild)}),o.appendChild(h),l.remove()}else o=null,r=null}return i.innerHTML}static postProcessHTMLManual(e,n){let i=e;i=i.replace(/((?:
    (?: )*
  • .*?<\/li><\/div>\s*)+)/gs,r=>{let s=r.match(/
    (?: )*
  • .*?<\/li><\/div>/gs)||[];return s.length>0?"
      "+s.map(c=>{let p=c.match(/
      ((?: )*)
    • .*?<\/li>/);if(p&&l){let d=p[1];return l[0].replace(/
    • /,`
    • ${d}`)}return l?l[0]:""}).filter(Boolean).join("")+"
    ":r}),i=i.replace(/((?:
    (?: )*
  • .*?<\/li><\/div>\s*)+)/gs,r=>{let s=r.match(/
    (?: )*
  • .*?<\/li><\/div>/gs)||[];return s.length>0?"
      "+s.map(c=>{let p=c.match(/
      ((?: )*)
    1. .*?<\/li>/);if(p&&l){let d=p[1];return l[0].replace(/
    2. /,`
    3. ${d}`)}return l?l[0]:""}).filter(Boolean).join("")+"
    ":r});let o=/
    (```[^<]*)<\/span><\/div>(.*?)
    (```)<\/span><\/div>/gs;return i=i.replace(o,(r,s,a,c)=>{let l=(a.match(/
    (.*?)<\/div>/gs)||[]).map(g=>g.replace(/
    (.*?)<\/div>/s,"$1").replace(/ /g," ")).join(` -`),d=s.slice(3).trim(),h=d?` class="language-${d}"`:"",u=l,f=n||this.codeHighlighter;if(f)try{let g=l.replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&"),y=f(g,d);y&&typeof y.then=="function"?console.warn("Async highlighters are not supported in Node.js (non-DOM) context. Use synchronous highlighters for server-side rendering."):y&&typeof y=="string"&&y.trim()&&(u=y)}catch(g){console.warn("Code highlighting failed:",g)}let m=`
    ${s}
    `;return m+=`
    ${u}
    `,m+=`
    ${c}
    `,m}),i}static getListContext(e,n){let i=e.split(` -`),o=0,r=0,s=0;for(let h=0;h=n){r=h,s=o;break}o+=u+1}let a=i[r],c=s+a.length,p=a.match(this.LIST_PATTERNS.checkbox);if(p)return{inList:!0,listType:"checkbox",indent:p[1],marker:"-",checked:p[2]==="x",content:p[3],lineStart:s,lineEnd:c,markerEndPos:s+p[1].length+p[2].length+5};let l=a.match(this.LIST_PATTERNS.bullet);if(l)return{inList:!0,listType:"bullet",indent:l[1],marker:l[2],content:l[3],lineStart:s,lineEnd:c,markerEndPos:s+l[1].length+l[2].length+1};let d=a.match(this.LIST_PATTERNS.numbered);return d?{inList:!0,listType:"numbered",indent:d[1],marker:parseInt(d[2]),content:d[3],lineStart:s,lineEnd:c,markerEndPos:s+d[1].length+d[2].length+2}:{inList:!1,listType:null,indent:"",marker:null,content:a,lineStart:s,lineEnd:c,markerEndPos:s}}static createNewListItem(e){switch(e.listType){case"bullet":return`${e.indent}${e.marker} `;case"numbered":return`${e.indent}${e.marker+1}. `;case"checkbox":return`${e.indent}- [ ] `;default:return""}}static renumberLists(e){let n=e.split(` -`),i=new Map,o=!1;return n.map(s=>{let a=s.match(this.LIST_PATTERNS.numbered);if(a){let c=a[1],p=c.length,l=a[3];o||i.clear();let d=(i.get(p)||0)+1;i.set(p,d);for(let[h]of i)h>p&&i.delete(h);return o=!0,`${c}${d}. ${l}`}else return(s.trim()===""||!s.match(/^\s/))&&(o=!1,i.clear()),s}).join(` -`)}};C(T,"linkIndex",0),C(T,"codeHighlighter",null),C(T,"customSyntax",null),C(T,"LIST_PATTERNS",{bullet:/^(\s*)([-*+])\s+(.*)$/,numbered:/^(\s*)(\d+)\.\s+(.*)$/,checkbox:/^(\s*)-\s+\[([ x])\]\s+(.*)$/});var re=class{constructor(e){this.editor=e}handleKeydown(e){if(!(navigator.platform.toLowerCase().includes("mac")?e.metaKey:e.ctrlKey))return!1;let o=null;switch(e.key.toLowerCase()){case"b":e.shiftKey||(o="toggleBold");break;case"i":e.shiftKey||(o="toggleItalic");break;case"k":e.shiftKey||(o="insertLink");break;case"7":e.shiftKey&&(o="toggleNumberedList");break;case"8":e.shiftKey&&(o="toggleBulletList");break}return o?(e.preventDefault(),this.editor.performAction(o,e),!0):!1}destroy(){}};var _={name:"solar",colors:{bgPrimary:"#faf0ca",bgSecondary:"#ffffff",text:"#0d3b66",textPrimary:"#0d3b66",textSecondary:"#5a7a9b",h1:"#f95738",h2:"#ee964b",h3:"#3d8a51",strong:"#ee964b",em:"#f95738",del:"#ee964b",link:"#0d3b66",code:"#0d3b66",codeBg:"rgba(244, 211, 94, 0.4)",blockquote:"#5a7a9b",hr:"#5a7a9b",syntaxMarker:"rgba(13, 59, 102, 0.52)",syntax:"#999999",cursor:"#f95738",selection:"rgba(244, 211, 94, 0.4)",listMarker:"#ee964b",rawLine:"#5a7a9b",border:"#e0e0e0",hoverBg:"#f0f0f0",primary:"#0d3b66",toolbarBg:"#ffffff",toolbarIcon:"#0d3b66",toolbarHover:"#f5f5f5",toolbarActive:"#faf0ca",placeholder:"#999999"},previewColors:{text:"#0d3b66",h1:"inherit",h2:"inherit",h3:"inherit",strong:"inherit",em:"inherit",link:"#0d3b66",code:"#0d3b66",codeBg:"rgba(244, 211, 94, 0.4)",blockquote:"#5a7a9b",hr:"#5a7a9b",bg:"transparent"}},qe={name:"cave",colors:{bgPrimary:"#141E26",bgSecondary:"#1D2D3E",text:"#c5dde8",textPrimary:"#c5dde8",textSecondary:"#9fcfec",h1:"#d4a5ff",h2:"#f6ae2d",h3:"#9fcfec",strong:"#f6ae2d",em:"#9fcfec",del:"#f6ae2d",link:"#9fcfec",code:"#c5dde8",codeBg:"#1a232b",blockquote:"#9fcfec",hr:"#c5dde8",syntaxMarker:"rgba(159, 207, 236, 0.73)",syntax:"#7a8c98",cursor:"#f26419",selection:"rgba(51, 101, 138, 0.4)",listMarker:"#f6ae2d",rawLine:"#9fcfec",border:"#2a3f52",hoverBg:"#243546",primary:"#9fcfec",toolbarBg:"#1D2D3E",toolbarIcon:"#c5dde8",toolbarHover:"#243546",toolbarActive:"#2a3f52",placeholder:"#6a7a88"},previewColors:{text:"#c5dde8",h1:"inherit",h2:"inherit",h3:"inherit",strong:"inherit",em:"inherit",link:"#9fcfec",code:"#c5dde8",codeBg:"#1a232b",blockquote:"#9fcfec",hr:"#c5dde8",bg:"transparent"}},We={solar:_,cave:qe,auto:_,light:_,dark:qe};function V(t){return typeof t=="string"?{...We[t]||We.solar,name:t}:t}function Oe(t){if(t!=="auto")return t;let e=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)");return e!=null&&e.matches?"cave":"solar"}function Q(t,e){let n=[];for(let[i,o]of Object.entries(t)){let r=i.replace(/([A-Z])/g,"-$1").toLowerCase();n.push(`--${r}: ${o};`)}if(e)for(let[i,o]of Object.entries(e)){let r=i.replace(/([A-Z])/g,"-$1").toLowerCase();n.push(`--preview-${r}-default: ${o};`)}return n.join(` -`)}function Ke(t,e={},n={}){return{...t,colors:{...t.colors,...e},previewColors:{...t.previewColors,...n}}}function Ze(t={}){let{fontSize:e="14px",lineHeight:n=1.6,fontFamily:i='"SF Mono", SFMono-Regular, Menlo, Monaco, "Cascadia Code", Consolas, "Roboto Mono", "Noto Sans Mono", "Droid Sans Mono", "Ubuntu Mono", "DejaVu Sans Mono", "Liberation Mono", "Courier New", Courier, monospace',padding:o="20px",theme:r=null,mobile:s={}}=t,a=Object.keys(s).length>0?` - @media (max-width: 640px) { - .overtype-wrapper .overtype-input, - .overtype-wrapper .overtype-preview { - ${Object.entries(s).map(([p,l])=>`${p.replace(/([A-Z])/g,"-$1").toLowerCase()}: ${l} !important;`).join(` - `)} - } - } - `:"",c=r&&r.colors?Q(r.colors,r.previewColors):"";return` - /* OverType Editor Styles */ - - /* Middle-ground CSS Reset - Prevent parent styles from leaking in */ - .overtype-container * { - /* Box model - these commonly leak */ - margin: 0 !important; - padding: 0 !important; - border: 0 !important; - - /* Layout - these can break our layout */ - /* Don't reset position - it breaks dropdowns */ - float: none !important; - clear: none !important; - - /* Typography - only reset decorative aspects */ - text-decoration: none !important; - text-transform: none !important; - letter-spacing: normal !important; - - /* Visual effects that can interfere */ - box-shadow: none !important; - text-shadow: none !important; - - /* Ensure box-sizing is consistent */ - box-sizing: border-box !important; - - /* Keep inheritance for these */ - /* font-family, color, line-height, font-size - inherit */ - } - - /* Container base styles after reset */ - .overtype-container { - display: flex !important; - flex-direction: column !important; - width: 100% !important; - height: 100% !important; - position: relative !important; /* Override reset - needed for absolute children */ - overflow: visible !important; /* Allow dropdown to overflow container */ - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; - text-align: left !important; - ${c?` - /* Theme Variables */ - ${c}`:""} - } - - /* Force left alignment for all elements in the editor */ - .overtype-container .overtype-wrapper * { - text-align: left !important; - } - - /* Auto-resize mode styles */ - .overtype-container.overtype-auto-resize { - height: auto !important; - } - - .overtype-container.overtype-auto-resize .overtype-wrapper { - flex: 0 0 auto !important; /* Don't grow/shrink, use explicit height */ - height: auto !important; - min-height: 60px !important; - overflow: visible !important; - } - - .overtype-wrapper { - position: relative !important; /* Override reset - needed for absolute children */ - width: 100% !important; - flex: 1 1 0 !important; /* Grow to fill remaining space, with flex-basis: 0 */ - min-height: 60px !important; /* Minimum usable height */ - overflow: hidden !important; - background: var(--bg-secondary, #ffffff) !important; - z-index: 1; /* Below toolbar and dropdown */ - } - - /* Critical alignment styles - must be identical for both layers */ - .overtype-wrapper .overtype-input, - .overtype-wrapper .overtype-preview { - /* Positioning - must be identical */ - position: absolute !important; /* Override reset - required for overlay */ - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; - - /* Font properties - any difference breaks alignment */ - font-family: ${i} !important; - font-variant-ligatures: none !important; /* keep metrics stable for code */ - font-size: var(--instance-font-size, ${e}) !important; - line-height: var(--instance-line-height, ${n}) !important; - font-weight: normal !important; - font-style: normal !important; - font-variant: normal !important; - font-stretch: normal !important; - font-kerning: none !important; - font-feature-settings: normal !important; - - /* Box model - must match exactly */ - padding: var(--instance-padding, ${o}) !important; - margin: 0 !important; - border: none !important; - outline: none !important; - box-sizing: border-box !important; - - /* Text layout - critical for character positioning */ - white-space: pre-wrap !important; - word-wrap: break-word !important; - word-break: normal !important; - overflow-wrap: break-word !important; - tab-size: 2 !important; - -moz-tab-size: 2 !important; - text-align: left !important; - text-indent: 0 !important; - letter-spacing: normal !important; - word-spacing: normal !important; - - /* Text rendering */ - text-transform: none !important; - text-rendering: auto !important; - -webkit-font-smoothing: auto !important; - -webkit-text-size-adjust: 100% !important; - - /* Direction and writing */ - direction: ltr !important; - writing-mode: horizontal-tb !important; - unicode-bidi: normal !important; - text-orientation: mixed !important; - - /* Visual effects that could shift perception */ - text-shadow: none !important; - filter: none !important; - transform: none !important; - zoom: 1 !important; - - /* Vertical alignment */ - vertical-align: baseline !important; - - /* Size constraints */ - min-width: 0 !important; - min-height: 0 !important; - max-width: none !important; - max-height: none !important; - - /* Overflow */ - overflow-y: auto !important; - overflow-x: auto !important; - /* overscroll-behavior removed to allow scroll-through to parent */ - scrollbar-width: auto !important; - scrollbar-gutter: auto !important; - - /* Animation/transition - disabled to prevent movement */ - animation: none !important; - transition: none !important; - } - - /* Input layer styles */ - .overtype-wrapper .overtype-input { - /* Layer positioning */ - z-index: 1 !important; - - /* Text visibility */ - color: transparent !important; - caret-color: var(--cursor, #f95738) !important; - background-color: transparent !important; - - /* Textarea-specific */ - resize: none !important; - appearance: none !important; - -webkit-appearance: none !important; - -moz-appearance: none !important; - - /* Prevent mobile zoom on focus */ - touch-action: manipulation !important; - - /* Disable autofill */ - autocomplete: off !important; - autocorrect: off !important; - autocapitalize: off !important; - } - - .overtype-wrapper .overtype-input::selection { - background-color: var(--selection, rgba(244, 211, 94, 0.4)); - } - - /* Placeholder shim - visible when textarea is empty */ - .overtype-wrapper .overtype-placeholder { - position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - z-index: 0 !important; - pointer-events: none !important; - user-select: none !important; - font-family: ${i} !important; - font-size: var(--instance-font-size, ${e}) !important; - line-height: var(--instance-line-height, ${n}) !important; - padding: var(--instance-padding, ${o}) !important; - box-sizing: border-box !important; - color: var(--placeholder, #999) !important; - overflow: hidden !important; - white-space: nowrap !important; - text-overflow: ellipsis !important; - } - - /* Preview layer styles */ - .overtype-wrapper .overtype-preview { - /* Layer positioning */ - z-index: 0 !important; - pointer-events: none !important; - color: var(--text, #0d3b66) !important; - background-color: transparent !important; - - /* Prevent text selection */ - user-select: none !important; - -webkit-user-select: none !important; - -moz-user-select: none !important; - -ms-user-select: none !important; - } - - /* Defensive styles for preview child divs */ - .overtype-wrapper .overtype-preview div { - /* Reset any inherited styles */ - margin: 0 !important; - padding: 0 !important; - border: none !important; - text-align: left !important; - text-indent: 0 !important; - display: block !important; - position: static !important; - transform: none !important; - min-height: 0 !important; - max-height: none !important; - line-height: inherit !important; - font-size: inherit !important; - font-family: inherit !important; - } - - /* Markdown element styling - NO SIZE CHANGES */ - .overtype-wrapper .overtype-preview .header { - font-weight: bold !important; - } - - /* Header colors */ - .overtype-wrapper .overtype-preview .h1 { - color: var(--h1, #f95738) !important; - } - .overtype-wrapper .overtype-preview .h2 { - color: var(--h2, #ee964b) !important; - } - .overtype-wrapper .overtype-preview .h3 { - color: var(--h3, #3d8a51) !important; - } - - /* Semantic headers - flatten in edit mode */ - .overtype-wrapper .overtype-preview h1, - .overtype-wrapper .overtype-preview h2, - .overtype-wrapper .overtype-preview h3 { - font-size: inherit !important; - font-weight: bold !important; - margin: 0 !important; - padding: 0 !important; - display: inline !important; - line-height: inherit !important; - } - - /* Header colors for semantic headers */ - .overtype-wrapper .overtype-preview h1 { - color: var(--h1, #f95738) !important; - } - .overtype-wrapper .overtype-preview h2 { - color: var(--h2, #ee964b) !important; - } - .overtype-wrapper .overtype-preview h3 { - color: var(--h3, #3d8a51) !important; - } - - /* Lists - remove styling in edit mode */ - .overtype-wrapper .overtype-preview ul, - .overtype-wrapper .overtype-preview ol { - list-style: none !important; - margin: 0 !important; - padding: 0 !important; - display: block !important; /* Lists need to be block for line breaks */ - } - - .overtype-wrapper .overtype-preview li { - display: block !important; /* Each item on its own line */ - margin: 0 !important; - padding: 0 !important; - /* Don't set list-style here - let ul/ol control it */ - } - - /* Bold text */ - .overtype-wrapper .overtype-preview strong { - color: var(--strong, #ee964b) !important; - font-weight: bold !important; - } - - /* Italic text */ - .overtype-wrapper .overtype-preview em { - color: var(--em, #f95738) !important; - text-decoration-color: var(--em, #f95738) !important; - text-decoration-thickness: 1px !important; - font-style: italic !important; - } - - /* Strikethrough text */ - .overtype-wrapper .overtype-preview del { - color: var(--del, #ee964b) !important; - text-decoration: line-through !important; - text-decoration-color: var(--del, #ee964b) !important; - text-decoration-thickness: 1px !important; - } - - /* Inline code */ - .overtype-wrapper .overtype-preview code { - background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important; - color: var(--code, #0d3b66) !important; - padding: 0 !important; - border-radius: 2px !important; - font-family: inherit !important; - font-size: inherit !important; - line-height: inherit !important; - font-weight: normal !important; - } - - /* Code blocks - consolidated pre blocks */ - .overtype-wrapper .overtype-preview pre { - padding: 0 !important; - margin: 0 !important; - border-radius: 4px !important; - overflow-x: auto !important; - } - - /* Code block styling in normal mode - yellow background */ - .overtype-wrapper .overtype-preview pre.code-block { - background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important; - white-space: break-spaces !important; /* Prevent horizontal scrollbar that breaks alignment */ - } - - /* Code inside pre blocks - remove background */ - .overtype-wrapper .overtype-preview pre code { - background: transparent !important; - color: var(--code, #0d3b66) !important; - font-family: ${i} !important; /* Match textarea font exactly for alignment */ - } - - /* Blockquotes */ - .overtype-wrapper .overtype-preview .blockquote { - color: var(--blockquote, #5a7a9b) !important; - padding: 0 !important; - margin: 0 !important; - border: none !important; - } - - /* Links */ - .overtype-wrapper .overtype-preview a { - color: var(--link, #0d3b66) !important; - text-decoration: underline !important; - font-weight: normal !important; - } - - .overtype-wrapper .overtype-preview a:hover { - text-decoration: underline !important; - color: var(--link, #0d3b66) !important; - } - - /* Lists - no list styling */ - .overtype-wrapper .overtype-preview ul, - .overtype-wrapper .overtype-preview ol { - list-style: none !important; - margin: 0 !important; - padding: 0 !important; - } - - - /* Horizontal rules */ - .overtype-wrapper .overtype-preview hr { - border: none !important; - color: var(--hr, #5a7a9b) !important; - margin: 0 !important; - padding: 0 !important; - } - - .overtype-wrapper .overtype-preview .hr-marker { - color: var(--hr, #5a7a9b) !important; - opacity: 0.6 !important; - } - - /* Code fence markers - with background when not in code block */ - .overtype-wrapper .overtype-preview .code-fence { - color: var(--code, #0d3b66) !important; - background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important; - } - - /* Code block lines - background for entire code block */ - .overtype-wrapper .overtype-preview .code-block-line { - background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important; - } - - /* Remove background from code fence when inside code block line */ - .overtype-wrapper .overtype-preview .code-block-line .code-fence { - background: transparent !important; - } - - /* Raw markdown line */ - .overtype-wrapper .overtype-preview .raw-line { - color: var(--raw-line, #5a7a9b) !important; - font-style: normal !important; - font-weight: normal !important; - } - - /* Syntax markers */ - .overtype-wrapper .overtype-preview .syntax-marker { - color: var(--syntax-marker, rgba(13, 59, 102, 0.52)) !important; - opacity: 0.7 !important; - } - - /* List markers */ - .overtype-wrapper .overtype-preview .list-marker { - color: var(--list-marker, #ee964b) !important; - } - - /* Stats bar */ - - /* Stats bar - positioned by flexbox */ - .overtype-stats { - height: 40px !important; - padding: 0 20px !important; - background: #f8f9fa !important; - border-top: 1px solid #e0e0e0 !important; - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; - font-size: 0.85rem !important; - color: #666 !important; - flex-shrink: 0 !important; /* Don't shrink */ - z-index: 10001 !important; /* Above link tooltip */ - position: relative !important; /* Enable z-index */ - } - - /* Dark theme stats bar */ - .overtype-container[data-theme="cave"] .overtype-stats { - background: var(--bg-secondary, #1D2D3E) !important; - border-top: 1px solid rgba(197, 221, 232, 0.1) !important; - color: var(--text, #c5dde8) !important; - } - - .overtype-stats .overtype-stat { - display: flex !important; - align-items: center !important; - gap: 5px !important; - white-space: nowrap !important; - } - - .overtype-stats .live-dot { - width: 8px !important; - height: 8px !important; - background: #4caf50 !important; - border-radius: 50% !important; - animation: overtype-pulse 2s infinite !important; - } - - @keyframes overtype-pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.6; transform: scale(1.2); } - } - - - /* Toolbar Styles */ - .overtype-toolbar.overtype-toolbar-hidden { - display: none !important; - } - - .overtype-toolbar { - display: flex !important; - align-items: center !important; - gap: 4px !important; - padding: 8px !important; /* Override reset */ - background: var(--toolbar-bg, var(--bg-primary, #f8f9fa)) !important; /* Override reset */ - border-bottom: 1px solid var(--toolbar-border, transparent) !important; /* Override reset */ - overflow-x: auto !important; /* Allow horizontal scrolling */ - overflow-y: hidden !important; /* Hide vertical overflow */ - -webkit-overflow-scrolling: touch !important; - flex-shrink: 0 !important; - height: auto !important; - position: relative !important; /* Override reset */ - z-index: 100 !important; /* Ensure toolbar is above wrapper */ - scrollbar-width: thin; /* Thin scrollbar on Firefox */ - } - - /* Thin scrollbar styling */ - .overtype-toolbar::-webkit-scrollbar { - height: 4px; - } - - .overtype-toolbar::-webkit-scrollbar-track { - background: transparent; - } - - .overtype-toolbar::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.2); - border-radius: 2px; - } - - .overtype-toolbar-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0; - border: none; - border-radius: 6px; - background: transparent; - color: var(--toolbar-icon, var(--text-secondary, #666)); - cursor: pointer; - transition: all 0.2s ease; - flex-shrink: 0; - } - - .overtype-toolbar-button svg { - width: 20px; - height: 20px; - fill: currentColor; - } - - .overtype-toolbar-button:hover { - background: var(--toolbar-hover, var(--bg-secondary, #e9ecef)); - color: var(--toolbar-icon, var(--text-primary, #333)); - } - - .overtype-toolbar-button:active { - transform: scale(0.95); - } - - .overtype-toolbar-button.active { - background: var(--toolbar-active, var(--primary, #007bff)); - color: var(--toolbar-icon, var(--text-primary, #333)); - } - - .overtype-toolbar-button:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .overtype-toolbar-separator { - width: 1px; - height: 24px; - background: var(--border, #e0e0e0); - margin: 0 4px; - flex-shrink: 0; - } - - /* Adjust wrapper when toolbar is present */ - /* Mobile toolbar adjustments */ - @media (max-width: 640px) { - .overtype-toolbar { - padding: 6px; - gap: 2px; - } - - .overtype-toolbar-button { - width: 36px; - height: 36px; - } - - .overtype-toolbar-separator { - margin: 0 2px; - } - } - - /* Plain mode - hide preview and show textarea text */ - .overtype-container[data-mode="plain"] .overtype-preview { - display: none !important; - } - - .overtype-container[data-mode="plain"] .overtype-input { - color: var(--text, #0d3b66) !important; - /* Use system font stack for better plain text readability */ - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Helvetica Neue", Arial, sans-serif !important; - } - - /* Ensure textarea remains transparent in overlay mode */ - .overtype-container:not([data-mode="plain"]) .overtype-input { - color: transparent !important; - } - - /* Dropdown menu styles */ - .overtype-toolbar-button { - position: relative !important; /* Override reset - needed for dropdown */ - } - - .overtype-toolbar-button.dropdown-active { - background: var(--toolbar-active, var(--hover-bg, #f0f0f0)); - } - - .overtype-dropdown-menu { - position: fixed !important; /* Fixed positioning relative to viewport */ - background: var(--bg-secondary, white) !important; /* Override reset */ - border: 1px solid var(--border, #e0e0e0) !important; /* Override reset */ - border-radius: 6px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; /* Override reset */ - z-index: 10000; /* Very high z-index to ensure visibility */ - min-width: 150px; - padding: 4px 0 !important; /* Override reset */ - /* Position will be set via JavaScript based on button position */ - } - - .overtype-dropdown-item { - display: flex; - align-items: center; - width: 100%; - padding: 8px 12px; - border: none; - background: none; - text-align: left; - cursor: pointer; - font-size: 14px; - color: var(--text, #333); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - } - - .overtype-dropdown-item:hover { - background: var(--hover-bg, #f0f0f0); - } - - .overtype-dropdown-item.active { - font-weight: 600; - } - - .overtype-dropdown-check { - width: 16px; - margin-right: 8px; - color: var(--h1, #007bff); - } - - .overtype-dropdown-icon { - width: 20px; - margin-right: 8px; - text-align: center; - } - - /* Preview mode styles */ - .overtype-container[data-mode="preview"] .overtype-input { - display: none !important; - } - - .overtype-container[data-mode="preview"] .overtype-preview { - pointer-events: auto !important; - user-select: text !important; - cursor: text !important; - } - - .overtype-container.overtype-auto-resize[data-mode="preview"] .overtype-preview { - position: static !important; - height: auto !important; - } - - /* Hide syntax markers in preview mode */ - .overtype-container[data-mode="preview"] .syntax-marker { - display: none !important; - } - - /* Hide URL part of links in preview mode - extra specificity */ - .overtype-container[data-mode="preview"] .syntax-marker.url-part, - .overtype-container[data-mode="preview"] .url-part { - display: none !important; - } - - /* Hide all syntax markers inside links too */ - .overtype-container[data-mode="preview"] a .syntax-marker { - display: none !important; - } - - /* Headers - restore proper sizing in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h1, - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h2, - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h3 { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; - font-weight: 600 !important; - margin: 0 !important; - display: block !important; - line-height: 1 !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h1 { - font-size: 2em !important; - color: var(--preview-h1, var(--preview-h1-default)) !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h2 { - font-size: 1.5em !important; - color: var(--preview-h2, var(--preview-h2-default)) !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview h3 { - font-size: 1.17em !important; - color: var(--preview-h3, var(--preview-h3-default)) !important; - } - - /* Lists - restore list styling in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview ul { - display: block !important; - list-style: disc !important; - padding-left: 2em !important; - margin: 1em 0 !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview ol { - display: block !important; - list-style: decimal !important; - padding-left: 2em !important; - margin: 1em 0 !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview li { - display: list-item !important; - margin: 0 !important; - padding: 0 !important; - } - - /* Task list checkboxes - only in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview li.task-list { - list-style: none !important; - position: relative !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview li.task-list input[type="checkbox"] { - margin-right: 0.5em !important; - cursor: default !important; - vertical-align: middle !important; - } - - /* Task list in normal mode - keep syntax visible */ - .overtype-container:not([data-mode="preview"]) .overtype-wrapper .overtype-preview li.task-list { - list-style: none !important; - } - - .overtype-container:not([data-mode="preview"]) .overtype-wrapper .overtype-preview li.task-list .syntax-marker { - color: var(--syntax, #999999) !important; - font-weight: normal !important; - } - - /* Links - make clickable in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview a { - pointer-events: auto !important; - cursor: pointer !important; - color: var(--preview-link, var(--preview-link-default)) !important; - text-decoration: underline !important; - } - - /* Code blocks - proper pre/code styling in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block { - background: var(--preview-code-bg, var(--preview-code-bg-default)) !important; - color: var(--preview-code, var(--preview-code-default)) !important; - padding: 1.2em !important; - border-radius: 3px !important; - overflow-x: auto !important; - margin: 0 !important; - display: block !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block code { - background: transparent !important; - color: inherit !important; - padding: 0 !important; - font-family: ${i} !important; - font-size: 0.9em !important; - line-height: 1.4 !important; - } - - /* Hide old code block lines and fences in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .code-block-line { - display: none !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .code-fence { - display: none !important; - } - - /* Blockquotes - enhanced styling in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .blockquote { - display: block !important; - border-left: 4px solid var(--preview-blockquote, var(--preview-blockquote-default)) !important; - color: var(--preview-blockquote, var(--preview-blockquote-default)) !important; - padding-left: 1em !important; - margin: 1em 0 !important; - font-style: italic !important; - } - - /* Typography improvements in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview { - font-family: Georgia, 'Times New Roman', serif !important; - font-size: 16px !important; - line-height: 1.8 !important; - color: var(--preview-text, var(--preview-text-default)) !important; - background: var(--preview-bg, var(--preview-bg-default)) !important; - } - - /* Inline code in preview mode - keep monospace */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview code { - font-family: ${i} !important; - font-size: 0.9em !important; - background: var(--preview-code-bg, var(--preview-code-bg-default)) !important; - color: var(--preview-code, var(--preview-code-default)) !important; - padding: 0.2em 0.4em !important; - border-radius: 3px !important; - } - - /* Strong and em elements in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview strong { - font-weight: 700 !important; - color: var(--preview-strong, var(--preview-strong-default)) !important; - } - - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview em { - font-style: italic !important; - color: var(--preview-em, var(--preview-em-default)) !important; - } - - /* HR in preview mode */ - .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview .hr-marker { - display: block !important; - border-top: 2px solid var(--preview-hr, var(--preview-hr-default)) !important; - text-indent: -9999px !important; - height: 2px !important; - } - - /* Link Tooltip */ - .overtype-link-tooltip { - background: #333 !important; - color: white !important; - padding: 6px 10px !important; - border-radius: 16px !important; - font-size: 12px !important; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; - display: flex !important; - visibility: hidden !important; - pointer-events: none !important; - z-index: 10000 !important; - cursor: pointer !important; - box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; - max-width: 300px !important; - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - position: fixed; - top: 0; - left: 0; - } - - .overtype-link-tooltip.visible { - visibility: visible !important; - pointer-events: auto !important; - } - - ${a} - `}var gn=Object.defineProperty,Qe=Object.getOwnPropertySymbols,vn=Object.prototype.hasOwnProperty,yn=Object.prototype.propertyIsEnumerable,Ge=(t,e,n)=>e in t?gn(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,Je=(t,e)=>{for(var n in e||(e={}))vn.call(e,n)&&Ge(t,n,e[n]);if(Qe)for(var n of Qe(e))yn.call(e,n)&&Ge(t,n,e[n]);return t},I={bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](url)",replaceNext:"url",scanFor:"https?://"},bulletList:{prefix:"- ",multiline:!0,unorderedList:!0},numberedList:{prefix:"1. ",multiline:!0,orderedList:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},taskList:{prefix:"- [ ] ",multiline:!0,surroundWithNewlines:!0},header1:{prefix:"# "},header2:{prefix:"## "},header3:{prefix:"### "},header4:{prefix:"#### "},header5:{prefix:"##### "},header6:{prefix:"###### "}};function wn(){return{prefix:"",suffix:"",blockPrefix:"",blockSuffix:"",multiline:!1,replaceNext:"",prefixSpace:!1,scanFor:"",surroundWithNewlines:!1,orderedList:!1,unorderedList:!1,trimFirst:!1}}function N(t){return Je(Je({},wn()),t)}var we=!1;function bn(){return we}function x(t,e,n){we&&(console.group(`\u{1F50D} ${t}`),console.log(e),n&&console.log("Data:",n),console.groupEnd())}function ye(t,e){if(!we)return;let n=t.value.slice(t.selectionStart,t.selectionEnd);console.group(`\u{1F4CD} Selection: ${e}`),console.log("Position:",`${t.selectionStart}-${t.selectionEnd}`),console.log("Selected text:",JSON.stringify(n)),console.log("Length:",n.length);let i=t.value.slice(Math.max(0,t.selectionStart-10),t.selectionStart),o=t.value.slice(t.selectionEnd,Math.min(t.value.length,t.selectionEnd+10));console.log("Context:",JSON.stringify(i)+"[SELECTION]"+JSON.stringify(o)),console.groupEnd()}function tt(t){we&&(console.group("\u{1F4DD} Result"),console.log("Text to insert:",JSON.stringify(t.text)),console.log("New selection:",`${t.selectionStart}-${t.selectionEnd}`),console.groupEnd())}var B=null;function U(t,{text:e,selectionStart:n,selectionEnd:i}){let o=bn();o&&(console.group("\u{1F527} insertText"),console.log("Current selection:",`${t.selectionStart}-${t.selectionEnd}`),console.log("Text to insert:",JSON.stringify(e)),console.log("New selection to set:",n,"-",i)),t.focus();let r=t.selectionStart,s=t.selectionEnd,a=t.value.slice(0,r),c=t.value.slice(s);o&&(console.log("Before text (last 20):",JSON.stringify(a.slice(-20))),console.log("After text (first 20):",JSON.stringify(c.slice(0,20))),console.log("Selected text being replaced:",JSON.stringify(t.value.slice(r,s))));let p=t.value,l=r!==s;if(B===null||B===!0){t.contentEditable="true";try{B=document.execCommand("insertText",!1,e),o&&console.log("execCommand returned:",B,"for text with",e.split(` -`).length,"lines")}catch(d){B=!1,o&&console.log("execCommand threw error:",d)}t.contentEditable="false"}if(o&&(console.log("canInsertText before:",B),console.log("execCommand result:",B)),B){let d=a+e+c,h=t.value;o&&(console.log("Expected length:",d.length),console.log("Actual length:",h.length)),h!==d&&o&&(console.log("execCommand changed the value but not as expected"),console.log("Expected:",JSON.stringify(d.slice(0,100))),console.log("Actual:",JSON.stringify(h.slice(0,100))))}if(!B)if(o&&console.log("Using manual insertion"),t.value===p){o&&console.log("Value unchanged, doing manual replacement");try{document.execCommand("ms-beginUndoUnit")}catch(d){}t.value=a+e+c;try{document.execCommand("ms-endUndoUnit")}catch(d){}t.dispatchEvent(new CustomEvent("input",{bubbles:!0,cancelable:!0}))}else o&&console.log("Value was changed by execCommand, skipping manual insertion");o&&console.log("Setting selection range:",n,i),n!=null&&i!=null?t.setSelectionRange(n,i):t.setSelectionRange(r,t.selectionEnd),o&&(console.log("Final value length:",t.value.length),console.groupEnd())}function Xe(t){return t.trim().split(` -`).length>1}function xn(t,e){let n=e;for(;t[n]&&t[n-1]!=null&&!t[n-1].match(/\s/);)n--;return n}function kn(t,e,n){let i=e,o=n?/\n/:/\s/;for(;t[i]&&!t[i].match(o);)i++;return i}function nt(t){let e=t.value.split(` -`),n=0;for(let i=0;i=n&&t.selectionStart=n&&t.selectionEnd0&&s[a-1]!==` -`;)a--;if(r){let p=i;for(;p0?`${s} -`:o,y=Xe(m)&&a&&a.length>0?` -${a}`:r;if(p){let k=t.value[t.selectionStart-1];t.selectionStart!==0&&k!=null&&!k.match(/\s/)&&(g=` ${g}`)}m=Ln(t,g,y,e.multiline);let w=t.selectionStart,b=t.selectionEnd,L=c&&c.length>0&&y.indexOf(c)>-1&&m.length>0;if(d){let k=$e(t);n=k.newlinesToAppend,i=k.newlinesToPrepend,g=n+o,y+=i}if(m.startsWith(g)&&m.endsWith(y)){let k=m.slice(g.length,m.length-y.length);if(u===f){let E=u-g.length;E=Math.max(E,w),E=Math.min(E,w+k.length),w=b=E}else b=w+k.length;return{text:k,selectionStart:w,selectionEnd:b}}else if(L)if(l&&l.length>0&&m.match(l)){y=y.replace(c,m);let k=g+y;return w=b=w+g.length,{text:k,selectionStart:w,selectionEnd:b}}else{let k=g+m+y;return w=w+g.length+m.length+y.indexOf(c),b=w+c.length,{text:k,selectionStart:w,selectionEnd:b}}else{let k=g+m+y;w=u+g.length,b=f+g.length;let E=m.match(/^\s*|\s*$/g);if(h&&E){let me=E[0]||"",Y=E[1]||"";k=me+g+m.trim()+y+Y,w+=me.length,b-=Y.length}return{text:k,selectionStart:w,selectionEnd:b}}}function it(t,e){let{prefix:n,suffix:i,surroundWithNewlines:o}=e,r=t.value.slice(t.selectionStart,t.selectionEnd),s=t.selectionStart,a=t.selectionEnd,c=r.split(` -`);if(c.every(l=>l.startsWith(n)&&(!i||l.endsWith(i))))r=c.map(l=>{let d=l.slice(n.length);return i&&(d=d.slice(0,d.length-i.length)),d}).join(` -`),a=s+r.length;else if(r=c.map(l=>n+l+(i||"")).join(` -`),o){let{newlinesToAppend:l,newlinesToPrepend:d}=$e(t);s+=l.length,a=s+r.length,r=l+r+d}return{text:r,selectionStart:s,selectionEnd:a}}function Ye(t){let e=t.split(` -`),n=/^\d+\.\s+/,i=e.every(r=>n.test(r)),o=e;return i&&(o=e.map(r=>r.replace(n,""))),{text:o.join(` -`),processed:i}}function et(t){let e=t.split(` -`),n="- ",i=e.every(r=>r.startsWith(n)),o=e;return i&&(o=e.map(r=>r.slice(n.length))),{text:o.join(` -`),processed:i}}function se(t,e){return e?"- ":`${t+1}. `}function Sn(t,e){let n,i,o;return t.orderedList?(n=Ye(e),i=et(n.text),o=i.text):(n=et(e),i=Ye(n.text),o=i.text),[n,i,o]}function En(t,e){let n=t.selectionStart===t.selectionEnd,i=t.selectionStart,o=t.selectionEnd;nt(t);let r=t.value.slice(t.selectionStart,t.selectionEnd),[s,a,c]=Sn(e,r),p=c.split(` -`).map((m,g)=>`${se(g,e.unorderedList)}${m}`),l=p.reduce((m,g,y)=>m+se(y,e.unorderedList).length,0),d=p.reduce((m,g,y)=>m+se(y,!e.unorderedList).length,0);if(s.processed)return n?(i=Math.max(i-se(0,e.unorderedList).length,0),o=i):(i=t.selectionStart,o=t.selectionEnd-l),{text:c,selectionStart:i,selectionEnd:o};let{newlinesToAppend:h,newlinesToPrepend:u}=$e(t),f=h+p.join(` -`)+u;return n?(i=Math.max(i+se(0,e.unorderedList).length+h.length,0),o=i):a.processed?(i=Math.max(t.selectionStart+h.length,0),o=t.selectionEnd+h.length+l-d):(i=Math.max(t.selectionStart+h.length,0),o=t.selectionEnd+h.length+l),{text:f,selectionStart:i,selectionEnd:o}}function ot(t,e){let n=be(t,i=>En(i,e),{adjustSelection:(i,o,r,s)=>{let a=t.value.slice(s,t.selectionEnd),c=/^\d+\.\s+/,p=/^- /,l=c.test(a),d=p.test(a),h=e.orderedList&&l||e.unorderedList&&d;if(o===r)if(h){let u=a.match(e.orderedList?c:p),f=u?u[0].length:0;return{start:Math.max(o-f,s),end:Math.max(o-f,s)}}else if(l||d){let u=a.match(l?c:p),f=u?u[0].length:0,g=(e.unorderedList?2:3)-f;return{start:o+g,end:o+g}}else{let u=e.unorderedList?2:3;return{start:o+u,end:o+u}}else if(h){let u=a.match(e.orderedList?c:p),f=u?u[0].length:0;return{start:Math.max(o-f,s),end:Math.max(r-f,s)}}else if(l||d){let u=a.match(l?c:p),f=u?u[0].length:0,g=(e.unorderedList?2:3)-f;return{start:o+g,end:r+g}}else{let u=e.unorderedList?2:3;return{start:o+u,end:r+u}}}});U(t,n)}function Tn(t){if(!t)return[];let e=[],{selectionStart:n,selectionEnd:i,value:o}=t,r=o.split(` -`),s=0,a="";for(let d of r){if(n>=s&&n<=s+d.length){a=d;break}s+=d.length+1}a.startsWith("- ")&&(a.startsWith("- [ ] ")||a.startsWith("- [x] ")?e.push("task-list"):e.push("bullet-list")),/^\d+\.\s/.test(a)&&e.push("numbered-list"),a.startsWith("> ")&&e.push("quote"),a.startsWith("# ")&&e.push("header"),a.startsWith("## ")&&e.push("header-2"),a.startsWith("### ")&&e.push("header-3");let c=Math.max(0,n-10),p=Math.min(o.length,i+10),l=o.slice(c,p);if(l.includes("**")){let d=o.slice(Math.max(0,n-100),n),h=o.slice(i,Math.min(o.length,i+100)),u=d.lastIndexOf("**"),f=h.indexOf("**");u!==-1&&f!==-1&&e.push("bold")}if(l.includes("_")){let d=o.slice(Math.max(0,n-100),n),h=o.slice(i,Math.min(o.length,i+100)),u=d.lastIndexOf("_"),f=h.indexOf("_");u!==-1&&f!==-1&&e.push("italic")}if(l.includes("`")){let d=o.slice(Math.max(0,n-100),n),h=o.slice(i,Math.min(o.length,i+100));d.includes("`")&&h.includes("`")&&e.push("code")}if(l.includes("[")&&l.includes("]")){let d=o.slice(Math.max(0,n-100),n),h=o.slice(i,Math.min(o.length,i+100)),u=d.lastIndexOf("["),f=h.indexOf("]");u!==-1&&f!==-1&&o.slice(i+f+1,i+f+10).startsWith("(")&&e.push("link")}return e}function rt(t){if(!t||t.disabled||t.readOnly)return;x("toggleBold","Starting"),ye(t,"Before");let e=N(I.bold),n=xe(t,e);tt(n),U(t,n),ye(t,"After")}function st(t){if(!t||t.disabled||t.readOnly)return;let e=N(I.italic),n=xe(t,e);U(t,n)}function at(t){if(!t||t.disabled||t.readOnly)return;let e=N(I.code),n=xe(t,e);U(t,n)}function lt(t,e={}){if(!t||t.disabled||t.readOnly)return;let n=t.value.slice(t.selectionStart,t.selectionEnd),i=N(I.link);if(n&&n.match(/^https?:\/\//)&&!e.url?(i.suffix=`](${n})`,i.replaceNext=""):e.url&&(i.suffix=`](${e.url})`,i.replaceNext=""),e.text&&!n){let s=t.selectionStart;t.value=t.value.slice(0,s)+e.text+t.value.slice(s),t.selectionStart=s,t.selectionEnd=s+e.text.length}let r=xe(t,i);U(t,r)}function ct(t){if(!t||t.disabled||t.readOnly)return;let e=N(I.bulletList);ot(t,e)}function pt(t){if(!t||t.disabled||t.readOnly)return;let e=N(I.numberedList);ot(t,e)}function dt(t){if(!t||t.disabled||t.readOnly)return;x("toggleQuote","Starting"),ye(t,"Initial");let e=N(I.quote),n=be(t,i=>it(i,e),{prefix:e.prefix});tt(n),U(t,n),ye(t,"Final")}function Ie(t){if(!t||t.disabled||t.readOnly)return;let e=N(I.taskList),n=be(t,i=>it(i,e),{prefix:e.prefix});U(t,n)}function Re(t,e=1,n=!1){if(!t||t.disabled||t.readOnly)return;(e<1||e>6)&&(e=1),x("insertHeader","============ START ============"),x("insertHeader",`Level: ${e}, Toggle: ${n}`),x("insertHeader",`Initial cursor: ${t.selectionStart}-${t.selectionEnd}`);let i=`header${e===1?"1":e}`,o=N(I[i]||I.header1);x("insertHeader",`Style prefix: "${o.prefix}"`);let r=t.value,s=t.selectionStart,a=t.selectionEnd,c=s;for(;c>0&&r[c-1]!==` -`;)c--;let p=a;for(;p{let y=g.value.slice(g.selectionStart,g.selectionEnd);x("insertHeader",`Line in operation: "${y}"`);let w=y.replace(/^#{1,6}\s*/,"");x("insertHeader",`Cleaned line: "${w}"`);let b;return f?(x("insertHeader","ACTION: Toggling OFF - removing header"),b=w):h>0?(x("insertHeader",`ACTION: Replacing H${h} with H${e}`),b=o.prefix+w):(x("insertHeader","ACTION: Adding new header"),b=o.prefix+w),x("insertHeader",`New line: "${b}"`),{text:b,selectionStart:g.selectionStart,selectionEnd:g.selectionEnd}},{prefix:o.prefix,adjustSelection:(g,y,w,b)=>{if(x("insertHeader","Adjusting selection:"),x("insertHeader",` - isRemoving param: ${g}`),x("insertHeader",` - shouldToggleOff: ${f}`),x("insertHeader",` - selStart: ${y}, selEnd: ${w}`),x("insertHeader",` - lineStartPos: ${b}`),f){let L=Math.max(y-u,b);return x("insertHeader",` - Removing header, adjusting by -${u}`),{start:L,end:y===w?L:Math.max(w-u,b)}}else if(u>0){let L=o.prefix.length-u;return x("insertHeader",` - Replacing header, adjusting by ${L}`),{start:y+L,end:w+L}}else return x("insertHeader",` - Adding header, adjusting by +${o.prefix.length}`),{start:y+o.prefix.length,end:w+o.prefix.length}}});x("insertHeader",`Final result: text="${m.text}", cursor=${m.selectionStart}-${m.selectionEnd}`),x("insertHeader","============ END ============"),U(t,m)}function ut(t){Re(t,1,!0)}function ht(t){Re(t,2,!0)}function ft(t){Re(t,3,!0)}function mt(t){return Tn(t)}var ke=class{constructor(e,n={}){this.editor=e,this.container=null,this.buttons={},this.toolbarButtons=n.toolbarButtons||[]}create(){this.container=document.createElement("div"),this.container.className="overtype-toolbar",this.container.setAttribute("role","toolbar"),this.container.setAttribute("aria-label","Formatting toolbar"),this.toolbarButtons.forEach(e=>{if(e.name==="separator"){let n=this.createSeparator();this.container.appendChild(n)}else{let n=this.createButton(e);this.buttons[e.name]=n,this.container.appendChild(n)}}),this.editor.container.insertBefore(this.container,this.editor.wrapper)}createSeparator(){let e=document.createElement("div");return e.className="overtype-toolbar-separator",e.setAttribute("role","separator"),e}createButton(e){let n=document.createElement("button");return n.className="overtype-toolbar-button",n.type="button",n.setAttribute("data-button",e.name),n.title=e.title||"",n.setAttribute("aria-label",e.title||e.name),n.innerHTML=this.sanitizeSVG(e.icon||""),e.name==="viewMode"?(n.classList.add("has-dropdown"),n.dataset.dropdown="true",n.addEventListener("click",i=>{i.preventDefault(),this.toggleViewModeDropdown(n)}),n):(n._clickHandler=i=>{i.preventDefault();let o=e.actionId||e.name;this.editor.performAction(o,i)},n.addEventListener("click",n._clickHandler),n)}async handleAction(e){if(e&&typeof e=="object"&&typeof e.action=="function"){this.editor.textarea.focus();try{return await e.action({editor:this.editor,getValue:()=>this.editor.getValue(),setValue:n=>this.editor.setValue(n),event:null}),!0}catch(n){return console.error(`Action "${e.name}" error:`,n),this.editor.wrapper.dispatchEvent(new CustomEvent("button-error",{detail:{buttonName:e.name,error:n}})),!1}}return typeof e=="string"?this.editor.performAction(e,null):!1}sanitizeSVG(e){return typeof e!="string"?"":e.replace(/)<[^<]*)*<\/script>/gi,"").replace(/\son\w+\s*=\s*["'][^"']*["']/gi,"").replace(/\son\w+\s*=\s*[^\s>]*/gi,"")}toggleViewModeDropdown(e){let n=document.querySelector(".overtype-dropdown-menu");if(n){n.remove(),e.classList.remove("dropdown-active");return}e.classList.add("dropdown-active");let i=this.createViewModeDropdown(e),o=e.getBoundingClientRect();i.style.position="absolute",i.style.top=`${o.bottom+5}px`,i.style.left=`${o.left}px`,document.body.appendChild(i),this.handleDocumentClick=r=>{!i.contains(r.target)&&!e.contains(r.target)&&(i.remove(),e.classList.remove("dropdown-active"),document.removeEventListener("click",this.handleDocumentClick))},setTimeout(()=>{document.addEventListener("click",this.handleDocumentClick)},0)}createViewModeDropdown(e){let n=document.createElement("div");n.className="overtype-dropdown-menu";let i=[{id:"normal",label:"Normal Edit",icon:"\u2713"},{id:"plain",label:"Plain Textarea",icon:"\u2713"},{id:"preview",label:"Preview Mode",icon:"\u2713"}],o=this.editor.container.dataset.mode||"normal";return i.forEach(r=>{let s=document.createElement("button");if(s.className="overtype-dropdown-item",s.type="button",s.textContent=r.label,r.id===o){s.classList.add("active"),s.setAttribute("aria-current","true");let a=document.createElement("span");a.className="overtype-dropdown-icon",a.textContent=r.icon,s.prepend(a)}s.addEventListener("click",a=>{switch(a.preventDefault(),r.id){case"plain":this.editor.showPlainTextarea();break;case"preview":this.editor.showPreviewMode();break;case"normal":default:this.editor.showNormalEditMode();break}n.remove(),e.classList.remove("dropdown-active"),document.removeEventListener("click",this.handleDocumentClick)}),n.appendChild(s)}),n}updateButtonStates(){var e;try{let n=((e=mt)==null?void 0:e(this.editor.textarea,this.editor.textarea.selectionStart))||[];Object.entries(this.buttons).forEach(([i,o])=>{if(i==="viewMode")return;let r=!1;switch(i){case"bold":r=n.includes("bold");break;case"italic":r=n.includes("italic");break;case"code":r=!1;break;case"bulletList":r=n.includes("bullet-list");break;case"orderedList":r=n.includes("numbered-list");break;case"taskList":r=n.includes("task-list");break;case"quote":r=n.includes("quote");break;case"h1":r=n.includes("header");break;case"h2":r=n.includes("header-2");break;case"h3":r=n.includes("header-3");break}o.classList.toggle("active",r),o.setAttribute("aria-pressed",r.toString())})}catch(n){}}show(){this.container&&this.container.classList.remove("overtype-toolbar-hidden")}hide(){this.container&&this.container.classList.add("overtype-toolbar-hidden")}destroy(){this.container&&(this.handleDocumentClick&&document.removeEventListener("click",this.handleDocumentClick),Object.values(this.buttons).forEach(e=>{e._clickHandler&&(e.removeEventListener("click",e._clickHandler),delete e._clickHandler)}),this.container.remove(),this.container=null,this.buttons={})}};var le=Math.min,q=Math.max,ce=Math.round;var O=t=>({x:t,y:t}),Cn={left:"right",right:"left",bottom:"top",top:"bottom"},An={start:"end",end:"start"};function _e(t,e,n){return q(t,le(e,n))}function pe(t,e){return typeof t=="function"?t(e):t}function W(t){return t.split("-")[0]}function de(t){return t.split("-")[1]}function Be(t){return t==="x"?"y":"x"}function Ne(t){return t==="y"?"height":"width"}var Mn=new Set(["top","bottom"]);function F(t){return Mn.has(W(t))?"y":"x"}function Fe(t){return Be(F(t))}function wt(t,e,n){n===void 0&&(n=!1);let i=de(t),o=Fe(t),r=Ne(o),s=o==="x"?i===(n?"end":"start")?"right":"left":i==="start"?"bottom":"top";return e.reference[r]>e.floating[r]&&(s=ae(s)),[s,ae(s)]}function bt(t){let e=ae(t);return[Le(t),e,Le(e)]}function Le(t){return t.replace(/start|end/g,e=>An[e])}var vt=["left","right"],yt=["right","left"],Hn=["top","bottom"],Pn=["bottom","top"];function On(t,e,n){switch(t){case"top":case"bottom":return n?e?yt:vt:e?vt:yt;case"left":case"right":return e?Hn:Pn;default:return[]}}function xt(t,e,n,i){let o=de(t),r=On(W(t),n==="start",i);return o&&(r=r.map(s=>s+"-"+o),e&&(r=r.concat(r.map(Le)))),r}function ae(t){return t.replace(/left|right|bottom|top/g,e=>Cn[e])}function $n(t){return{top:0,right:0,bottom:0,left:0,...t}}function kt(t){return typeof t!="number"?$n(t):{top:t,right:t,bottom:t,left:t}}function G(t){let{x:e,y:n,width:i,height:o}=t;return{width:i,height:o,top:n,left:e,right:e+i,bottom:n+o,x:e,y:n}}function Lt(t,e,n){let{reference:i,floating:o}=t,r=F(e),s=Fe(e),a=Ne(s),c=W(e),p=r==="y",l=i.x+i.width/2-o.width/2,d=i.y+i.height/2-o.height/2,h=i[a]/2-o[a]/2,u;switch(c){case"top":u={x:l,y:i.y-o.height};break;case"bottom":u={x:l,y:i.y+i.height};break;case"right":u={x:i.x+i.width,y:d};break;case"left":u={x:i.x-o.width,y:d};break;default:u={x:i.x,y:i.y}}switch(de(e)){case"start":u[s]-=h*(n&&p?-1:1);break;case"end":u[s]+=h*(n&&p?-1:1);break}return u}async function St(t,e){var n;e===void 0&&(e={});let{x:i,y:o,platform:r,rects:s,elements:a,strategy:c}=t,{boundary:p="clippingAncestors",rootBoundary:l="viewport",elementContext:d="floating",altBoundary:h=!1,padding:u=0}=pe(e,t),f=kt(u),g=a[h?d==="floating"?"reference":"floating":d],y=G(await r.getClippingRect({element:(n=await(r.isElement==null?void 0:r.isElement(g)))==null||n?g:g.contextElement||await(r.getDocumentElement==null?void 0:r.getDocumentElement(a.floating)),boundary:p,rootBoundary:l,strategy:c})),w=d==="floating"?{x:i,y:o,width:s.floating.width,height:s.floating.height}:s.reference,b=await(r.getOffsetParent==null?void 0:r.getOffsetParent(a.floating)),L=await(r.isElement==null?void 0:r.isElement(b))?await(r.getScale==null?void 0:r.getScale(b))||{x:1,y:1}:{x:1,y:1},k=G(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:a,rect:w,offsetParent:b,strategy:c}):w);return{top:(y.top-k.top+f.top)/L.y,bottom:(k.bottom-y.bottom+f.bottom)/L.y,left:(y.left-k.left+f.left)/L.x,right:(k.right-y.right+f.right)/L.x}}var Et=async(t,e,n)=>{let{placement:i="bottom",strategy:o="absolute",middleware:r=[],platform:s}=n,a=r.filter(Boolean),c=await(s.isRTL==null?void 0:s.isRTL(e)),p=await s.getElementRects({reference:t,floating:e,strategy:o}),{x:l,y:d}=Lt(p,i,c),h=i,u={},f=0;for(let g=0;gK<=0)){var je,Ve;let K=(((je=r.flip)==null?void 0:je.index)||0)+1,Pe=me[K];if(Pe&&(!(d==="alignment"?w!==F(Pe):!1)||ee.every(P=>F(P.placement)===w?P.overflows[0]>0:!0)))return{data:{index:K,overflows:ee},reset:{placement:Pe}};let oe=(Ve=ee.filter(Z=>Z.overflows[0]<=0).sort((Z,P)=>Z.overflows[1]-P.overflows[1])[0])==null?void 0:Ve.placement;if(!oe)switch(u){case"bestFit":{var Ue;let Z=(Ue=ee.filter(P=>{if(E){let j=F(P.placement);return j===w||j==="y"}return!0}).map(P=>[P.placement,P.overflows.filter(j=>j>0).reduce((j,ln)=>j+ln,0)]).sort((P,j)=>P[1]-j[1])[0])==null?void 0:Ue[0];Z&&(oe=Z);break}case"initialPlacement":oe=a;break}if(o!==oe)return{reset:{placement:oe}}}return{}}}};var In=new Set(["left","top"]);async function Rn(t,e){let{placement:n,platform:i,elements:o}=t,r=await(i.isRTL==null?void 0:i.isRTL(o.floating)),s=W(n),a=de(n),c=F(n)==="y",p=In.has(s)?-1:1,l=r&&c?-1:1,d=pe(e,t),{mainAxis:h,crossAxis:u,alignmentAxis:f}=typeof d=="number"?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return a&&typeof f=="number"&&(u=a==="end"?f*-1:f),c?{x:u*l,y:h*p}:{x:h*p,y:u*l}}var Ct=function(t){return t===void 0&&(t=0),{name:"offset",options:t,async fn(e){var n,i;let{x:o,y:r,placement:s,middlewareData:a}=e,c=await Rn(e,t);return s===((n=a.offset)==null?void 0:n.placement)&&(i=a.arrow)!=null&&i.alignmentOffset?{}:{x:o+c.x,y:r+c.y,data:{...c,placement:s}}}}},At=function(t){return t===void 0&&(t={}),{name:"shift",options:t,async fn(e){let{x:n,y:i,placement:o,platform:r}=e,{mainAxis:s=!0,crossAxis:a=!1,limiter:c={fn:y=>{let{x:w,y:b}=y;return{x:w,y:b}}},...p}=pe(t,e),l={x:n,y:i},d=await r.detectOverflow(e,p),h=F(W(o)),u=Be(h),f=l[u],m=l[h];if(s){let y=u==="y"?"top":"left",w=u==="y"?"bottom":"right",b=f+d[y],L=f-d[w];f=_e(b,f,L)}if(a){let y=h==="y"?"top":"left",w=h==="y"?"bottom":"right",b=m+d[y],L=m-d[w];m=_e(b,m,L)}let g=c.fn({...e,[u]:f,[h]:m});return{...g,data:{x:g.x-n,y:g.y-i,enabled:{[u]:s,[h]:a}}}}}};function Ee(){return typeof window<"u"}function J(t){return Ht(t)?(t.nodeName||"").toLowerCase():"#document"}function A(t){var e;return(t==null||(e=t.ownerDocument)==null?void 0:e.defaultView)||window}function R(t){var e;return(e=(Ht(t)?t.ownerDocument:t.document)||window.document)==null?void 0:e.documentElement}function Ht(t){return Ee()?t instanceof Node||t instanceof A(t).Node:!1}function M(t){return Ee()?t instanceof Element||t instanceof A(t).Element:!1}function $(t){return Ee()?t instanceof HTMLElement||t instanceof A(t).HTMLElement:!1}function Mt(t){return!Ee()||typeof ShadowRoot>"u"?!1:t instanceof ShadowRoot||t instanceof A(t).ShadowRoot}var _n=new Set(["inline","contents"]);function te(t){let{overflow:e,overflowX:n,overflowY:i,display:o}=H(t);return/auto|scroll|overlay|hidden|clip/.test(e+i+n)&&!_n.has(o)}var Bn=new Set(["table","td","th"]);function Pt(t){return Bn.has(J(t))}var Nn=[":popover-open",":modal"];function ue(t){return Nn.some(e=>{try{return t.matches(e)}catch(n){return!1}})}var Fn=["transform","translate","scale","rotate","perspective"],Dn=["transform","translate","scale","rotate","perspective","filter"],zn=["paint","layout","strict","content"];function Te(t){let e=Ce(),n=M(t)?H(t):t;return Fn.some(i=>n[i]?n[i]!=="none":!1)||(n.containerType?n.containerType!=="normal":!1)||!e&&(n.backdropFilter?n.backdropFilter!=="none":!1)||!e&&(n.filter?n.filter!=="none":!1)||Dn.some(i=>(n.willChange||"").includes(i))||zn.some(i=>(n.contain||"").includes(i))}function Ot(t){let e=D(t);for(;$(e)&&!X(e);){if(Te(e))return e;if(ue(e))return null;e=D(e)}return null}function Ce(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}var jn=new Set(["html","body","#document"]);function X(t){return jn.has(J(t))}function H(t){return A(t).getComputedStyle(t)}function he(t){return M(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.scrollX,scrollTop:t.scrollY}}function D(t){if(J(t)==="html")return t;let e=t.assignedSlot||t.parentNode||Mt(t)&&t.host||R(t);return Mt(e)?e.host:e}function $t(t){let e=D(t);return X(e)?t.ownerDocument?t.ownerDocument.body:t.body:$(e)&&te(e)?e:$t(e)}function Se(t,e,n){var i;e===void 0&&(e=[]),n===void 0&&(n=!0);let o=$t(t),r=o===((i=t.ownerDocument)==null?void 0:i.body),s=A(o);if(r){let a=Ae(s);return e.concat(s,s.visualViewport||[],te(o)?o:[],a&&n?Se(a):[])}return e.concat(o,Se(o,[],n))}function Ae(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function Bt(t){let e=H(t),n=parseFloat(e.width)||0,i=parseFloat(e.height)||0,o=$(t),r=o?t.offsetWidth:n,s=o?t.offsetHeight:i,a=ce(n)!==r||ce(i)!==s;return a&&(n=r,i=s),{width:n,height:i,$:a}}function Nt(t){return M(t)?t:t.contextElement}function ne(t){let e=Nt(t);if(!$(e))return O(1);let n=e.getBoundingClientRect(),{width:i,height:o,$:r}=Bt(e),s=(r?ce(n.width):n.width)/i,a=(r?ce(n.height):n.height)/o;return(!s||!Number.isFinite(s))&&(s=1),(!a||!Number.isFinite(a))&&(a=1),{x:s,y:a}}var Vn=O(0);function Ft(t){let e=A(t);return!Ce()||!e.visualViewport?Vn:{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}}function Un(t,e,n){return e===void 0&&(e=!1),!n||e&&n!==A(t)?!1:e}function fe(t,e,n,i){e===void 0&&(e=!1),n===void 0&&(n=!1);let o=t.getBoundingClientRect(),r=Nt(t),s=O(1);e&&(i?M(i)&&(s=ne(i)):s=ne(t));let a=Un(r,n,i)?Ft(r):O(0),c=(o.left+a.x)/s.x,p=(o.top+a.y)/s.y,l=o.width/s.x,d=o.height/s.y;if(r){let h=A(r),u=i&&M(i)?A(i):i,f=h,m=Ae(f);for(;m&&i&&u!==f;){let g=ne(m),y=m.getBoundingClientRect(),w=H(m),b=y.left+(m.clientLeft+parseFloat(w.paddingLeft))*g.x,L=y.top+(m.clientTop+parseFloat(w.paddingTop))*g.y;c*=g.x,p*=g.y,l*=g.x,d*=g.y,c+=b,p+=L,f=A(m),m=Ae(f)}}return G({width:l,height:d,x:c,y:p})}function Me(t,e){let n=he(t).scrollLeft;return e?e.left+n:fe(R(t)).left+n}function Dt(t,e){let n=t.getBoundingClientRect(),i=n.left+e.scrollLeft-Me(t,n),o=n.top+e.scrollTop;return{x:i,y:o}}function qn(t){let{elements:e,rect:n,offsetParent:i,strategy:o}=t,r=o==="fixed",s=R(i),a=e?ue(e.floating):!1;if(i===s||a&&r)return n;let c={scrollLeft:0,scrollTop:0},p=O(1),l=O(0),d=$(i);if((d||!d&&!r)&&((J(i)!=="body"||te(s))&&(c=he(i)),$(i))){let u=fe(i);p=ne(i),l.x=u.x+i.clientLeft,l.y=u.y+i.clientTop}let h=s&&!d&&!r?Dt(s,c):O(0);return{width:n.width*p.x,height:n.height*p.y,x:n.x*p.x-c.scrollLeft*p.x+l.x+h.x,y:n.y*p.y-c.scrollTop*p.y+l.y+h.y}}function Wn(t){return Array.from(t.getClientRects())}function Kn(t){let e=R(t),n=he(t),i=t.ownerDocument.body,o=q(e.scrollWidth,e.clientWidth,i.scrollWidth,i.clientWidth),r=q(e.scrollHeight,e.clientHeight,i.scrollHeight,i.clientHeight),s=-n.scrollLeft+Me(t),a=-n.scrollTop;return H(i).direction==="rtl"&&(s+=q(e.clientWidth,i.clientWidth)-o),{width:o,height:r,x:s,y:a}}var It=25;function Zn(t,e){let n=A(t),i=R(t),o=n.visualViewport,r=i.clientWidth,s=i.clientHeight,a=0,c=0;if(o){r=o.width,s=o.height;let l=Ce();(!l||l&&e==="fixed")&&(a=o.offsetLeft,c=o.offsetTop)}let p=Me(i);if(p<=0){let l=i.ownerDocument,d=l.body,h=getComputedStyle(d),u=l.compatMode==="CSS1Compat"&&parseFloat(h.marginLeft)+parseFloat(h.marginRight)||0,f=Math.abs(i.clientWidth-d.clientWidth-u);f<=It&&(r-=f)}else p<=It&&(r+=p);return{width:r,height:s,x:a,y:c}}var Qn=new Set(["absolute","fixed"]);function Gn(t,e){let n=fe(t,!0,e==="fixed"),i=n.top+t.clientTop,o=n.left+t.clientLeft,r=$(t)?ne(t):O(1),s=t.clientWidth*r.x,a=t.clientHeight*r.y,c=o*r.x,p=i*r.y;return{width:s,height:a,x:c,y:p}}function Rt(t,e,n){let i;if(e==="viewport")i=Zn(t,n);else if(e==="document")i=Kn(R(t));else if(M(e))i=Gn(e,n);else{let o=Ft(t);i={x:e.x-o.x,y:e.y-o.y,width:e.width,height:e.height}}return G(i)}function zt(t,e){let n=D(t);return n===e||!M(n)||X(n)?!1:H(n).position==="fixed"||zt(n,e)}function Jn(t,e){let n=e.get(t);if(n)return n;let i=Se(t,[],!1).filter(a=>M(a)&&J(a)!=="body"),o=null,r=H(t).position==="fixed",s=r?D(t):t;for(;M(s)&&!X(s);){let a=H(s),c=Te(s);!c&&a.position==="fixed"&&(o=null),(r?!c&&!o:!c&&a.position==="static"&&!!o&&Qn.has(o.position)||te(s)&&!c&&zt(t,s))?i=i.filter(l=>l!==s):o=a,s=D(s)}return e.set(t,i),i}function Xn(t){let{element:e,boundary:n,rootBoundary:i,strategy:o}=t,s=[...n==="clippingAncestors"?ue(e)?[]:Jn(e,this._c):[].concat(n),i],a=s[0],c=s.reduce((p,l)=>{let d=Rt(e,l,o);return p.top=q(d.top,p.top),p.right=le(d.right,p.right),p.bottom=le(d.bottom,p.bottom),p.left=q(d.left,p.left),p},Rt(e,a,o));return{width:c.right-c.left,height:c.bottom-c.top,x:c.left,y:c.top}}function Yn(t){let{width:e,height:n}=Bt(t);return{width:e,height:n}}function ei(t,e,n){let i=$(e),o=R(e),r=n==="fixed",s=fe(t,!0,r,e),a={scrollLeft:0,scrollTop:0},c=O(0);function p(){c.x=Me(o)}if(i||!i&&!r)if((J(e)!=="body"||te(o))&&(a=he(e)),i){let u=fe(e,!0,r,e);c.x=u.x+e.clientLeft,c.y=u.y+e.clientTop}else o&&p();r&&!i&&o&&p();let l=o&&!i&&!r?Dt(o,a):O(0),d=s.left+a.scrollLeft-c.x-l.x,h=s.top+a.scrollTop-c.y-l.y;return{x:d,y:h,width:s.width,height:s.height}}function De(t){return H(t).position==="static"}function _t(t,e){if(!$(t)||H(t).position==="fixed")return null;if(e)return e(t);let n=t.offsetParent;return R(t)===n&&(n=n.ownerDocument.body),n}function jt(t,e){let n=A(t);if(ue(t))return n;if(!$(t)){let o=D(t);for(;o&&!X(o);){if(M(o)&&!De(o))return o;o=D(o)}return n}let i=_t(t,e);for(;i&&Pt(i)&&De(i);)i=_t(i,e);return i&&X(i)&&De(i)&&!Te(i)?n:i||Ot(t)||n}var ti=async function(t){let e=this.getOffsetParent||jt,n=this.getDimensions,i=await n(t.floating);return{reference:ei(t.reference,await e(t.floating),t.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}};function ni(t){return H(t).direction==="rtl"}var ii={convertOffsetParentRelativeRectToViewportRelativeRect:qn,getDocumentElement:R,getClippingRect:Xn,getOffsetParent:jt,getElementRects:ti,getClientRects:Wn,getDimensions:Yn,getScale:ne,isElement:M,isRTL:ni};var Vt=Ct;var Ut=At,qt=Tt;var Wt=(t,e,n)=>{let i=new Map,o={platform:ii,...n},r={...o.platform,_c:i};return Et(t,e,{...o,platform:r})};var He=class{constructor(e){this.editor=e,this.tooltip=null,this.currentLink=null,this.hideTimeout=null,this.visibilityChangeHandler=null,this.isTooltipHovered=!1,this.init()}init(){this.createTooltip(),this.editor.textarea.addEventListener("selectionchange",()=>this.checkCursorPosition()),this.editor.textarea.addEventListener("keyup",e=>{(e.key.includes("Arrow")||e.key==="Home"||e.key==="End")&&this.checkCursorPosition()}),this.editor.textarea.addEventListener("input",()=>this.hide()),this.editor.textarea.addEventListener("scroll",()=>{this.currentLink&&this.positionTooltip(this.currentLink)}),this.editor.textarea.addEventListener("blur",()=>{this.isTooltipHovered||this.hide()}),this.visibilityChangeHandler=()=>{document.hidden&&this.hide()},document.addEventListener("visibilitychange",this.visibilityChangeHandler),this.tooltip.addEventListener("mouseenter",()=>{this.isTooltipHovered=!0,this.cancelHide()}),this.tooltip.addEventListener("mouseleave",()=>{this.isTooltipHovered=!1,this.scheduleHide()})}createTooltip(){this.tooltip=document.createElement("div"),this.tooltip.className="overtype-link-tooltip",this.tooltip.innerHTML=` - - - - - - - - `,this.tooltip.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation(),this.currentLink&&(window.open(this.currentLink.url,"_blank"),this.hide())}),this.editor.container.appendChild(this.tooltip)}checkCursorPosition(){let e=this.editor.textarea.selectionStart,n=this.editor.textarea.value,i=this.findLinkAtPosition(n,e);i?(!this.currentLink||this.currentLink.url!==i.url||this.currentLink.index!==i.index)&&this.show(i):this.scheduleHide()}findLinkAtPosition(e,n){let i=/\[([^\]]+)\]\(([^)]+)\)/g,o,r=0;for(;(o=i.exec(e))!==null;){let s=o.index,a=o.index+o[0].length;if(n>=s&&n<=a)return{text:o[1],url:o[2],index:r,start:s,end:a};r++}return null}async show(e){this.currentLink=e,this.cancelHide();let n=this.tooltip.querySelector(".overtype-link-tooltip-url");n.textContent=e.url,await this.positionTooltip(e),this.currentLink===e&&this.tooltip.classList.add("visible")}async positionTooltip(e){let n=this.findAnchorElement(e.index);if(!n)return;let i=n.getBoundingClientRect();if(!(i.width===0||i.height===0))try{let{x:o,y:r}=await Wt(n,this.tooltip,{strategy:"fixed",placement:"bottom",middleware:[Vt(8),Ut({padding:8}),qt()]});Object.assign(this.tooltip.style,{left:`${o}px`,top:`${r}px`,position:"fixed"})}catch(o){console.warn("Floating UI positioning failed:",o)}}findAnchorElement(e){return this.editor.preview.querySelector(`a[style*="--link-${e}"]`)}hide(){this.tooltip.classList.remove("visible"),this.currentLink=null,this.isTooltipHovered=!1}scheduleHide(){this.cancelHide(),this.hideTimeout=setTimeout(()=>this.hide(),300)}cancelHide(){this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=null)}destroy(){this.cancelHide(),this.visibilityChangeHandler&&(document.removeEventListener("visibilitychange",this.visibilityChangeHandler),this.visibilityChangeHandler=null),this.tooltip&&this.tooltip.parentNode&&this.tooltip.parentNode.removeChild(this.tooltip),this.tooltip=null,this.currentLink=null,this.isTooltipHovered=!1}};var Kt=` - - -`,Zt=` - - - -`,Qt=` - -`,Gt=` - -`,Jt=` - -`,Xt=` - - - -`,Yt=` - - - -`,en=` - - - - - - -`,tn=` - - - - - - - -`,nn=` - - -`,on=` - - - - - - -`,rn=` - - - -`,sn=` - - -`;var S={bold:{name:"bold",actionId:"toggleBold",icon:Kt,title:"Bold (Ctrl+B)",action:({editor:t})=>{rt(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},italic:{name:"italic",actionId:"toggleItalic",icon:Zt,title:"Italic (Ctrl+I)",action:({editor:t})=>{st(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},code:{name:"code",actionId:"toggleCode",icon:Yt,title:"Inline Code",action:({editor:t})=>{at(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},separator:{name:"separator"},link:{name:"link",actionId:"insertLink",icon:Xt,title:"Insert Link",action:({editor:t})=>{lt(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},h1:{name:"h1",actionId:"toggleH1",icon:Qt,title:"Heading 1",action:({editor:t})=>{ut(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},h2:{name:"h2",actionId:"toggleH2",icon:Gt,title:"Heading 2",action:({editor:t})=>{ht(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},h3:{name:"h3",actionId:"toggleH3",icon:Jt,title:"Heading 3",action:({editor:t})=>{ft(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},bulletList:{name:"bulletList",actionId:"toggleBulletList",icon:en,title:"Bullet List",action:({editor:t})=>{ct(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},orderedList:{name:"orderedList",actionId:"toggleNumberedList",icon:tn,title:"Numbered List",action:({editor:t})=>{pt(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},taskList:{name:"taskList",actionId:"toggleTaskList",icon:on,title:"Task List",action:({editor:t})=>{Ie&&(Ie(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0})))}},quote:{name:"quote",actionId:"toggleQuote",icon:nn,title:"Quote",action:({editor:t})=>{dt(t.textarea),t.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}},upload:{name:"upload",actionId:"uploadFile",icon:rn,title:"Upload File",action:({editor:t})=>{var n,i;if(!((n=t.options.fileUpload)!=null&&n.enabled))return;let e=document.createElement("input");e.type="file",e.multiple=!0,((i=t.options.fileUpload.mimeTypes)==null?void 0:i.length)>0&&(e.accept=t.options.fileUpload.mimeTypes.join(",")),e.onchange=()=>{var r;if(!((r=e.files)!=null&&r.length))return;let o=new DataTransfer;for(let s of e.files)o.items.add(s);t._handleDataTransfer(o)},e.click()}},viewMode:{name:"viewMode",icon:sn,title:"View mode"}},ie=[S.bold,S.italic,S.code,S.separator,S.link,S.separator,S.h1,S.h2,S.h3,S.separator,S.bulletList,S.orderedList,S.taskList,S.separator,S.quote,S.separator,S.viewMode];function ze(t){let e={};return(t||[]).forEach(n=>{if(!n||n.name==="separator")return;let i=n.actionId||n.name;n.action&&(e[i]=n.action)}),e}function an(t){let e=t||ie;return Array.isArray(e)?e.map(n=>({name:(n==null?void 0:n.name)||null,actionId:(n==null?void 0:n.actionId)||(n==null?void 0:n.name)||null,icon:(n==null?void 0:n.icon)||null,title:(n==null?void 0:n.title)||null})):null}function ri(t,e){let n=an(t),i=an(e);if(n===null||i===null)return n!==i;if(n.length!==i.length)return!0;for(let o=0;o{if(r.overTypeInstance)return r.overTypeInstance.reinit(n),r.overTypeInstance;let s=Object.create(v.prototype);return s._init(r,n),r.overTypeInstance=s,v.instances.set(r,s),s})}_init(e,n={}){this.element=e,this.instanceTheme=n.theme||null,this.options=this._mergeOptions(n),this.instanceId=++v.instanceCount,this.initialized=!1,v.injectStyles(),v.initGlobalListeners();let i=e.querySelector(".overtype-container"),o=e.querySelector(".overtype-wrapper");i||o?this._recoverFromDOM(i,o):this._buildFromScratch(),this.instanceTheme==="auto"&&this.setTheme("auto"),this.shortcuts=new re(this),this._rebuildActionsMap(),this.linkTooltip=new He(this),requestAnimationFrame(()=>{requestAnimationFrame(()=>{this.textarea.scrollTop=this.preview.scrollTop,this.textarea.scrollLeft=this.preview.scrollLeft})}),this.initialized=!0,this.options.onChange&&this.options.onChange(this.getValue(),this)}_mergeOptions(e){let n={fontSize:"14px",lineHeight:1.6,fontFamily:'"SF Mono", SFMono-Regular, Menlo, Monaco, "Cascadia Code", Consolas, "Roboto Mono", "Noto Sans Mono", "Droid Sans Mono", "Ubuntu Mono", "DejaVu Sans Mono", "Liberation Mono", "Courier New", Courier, monospace',padding:"16px",mobile:{fontSize:"16px",padding:"12px",lineHeight:1.5},textareaProps:{},autofocus:!1,autoResize:!1,minHeight:"100px",maxHeight:null,placeholder:"Start typing...",value:"",onChange:null,onKeydown:null,showActiveLineRaw:!1,showStats:!1,toolbar:!1,toolbarButtons:null,statsFormatter:null,smartLists:!0,codeHighlighter:null,spellcheck:!1},{theme:i,colors:o,...r}=e;return{...n,...r}}_recoverFromDOM(e,n){if(e&&e.classList.contains("overtype-container"))this.container=e,this.wrapper=e.querySelector(".overtype-wrapper");else if(n){this.wrapper=n,this.container=document.createElement("div"),this.container.className="overtype-container";let i=this.instanceTheme||v.currentTheme||_,o=typeof i=="string"?i:i.name;if(o&&this.container.setAttribute("data-theme",o),this.instanceTheme){let r=typeof this.instanceTheme=="string"?V(this.instanceTheme):this.instanceTheme;if(r&&r.colors){let s=Q(r.colors);this.container.style.cssText+=s}}n.parentNode.insertBefore(this.container,n),this.container.appendChild(n)}if(!this.wrapper){e&&e.remove(),n&&n.remove(),this._buildFromScratch();return}if(this.textarea=this.wrapper.querySelector(".overtype-input"),this.preview=this.wrapper.querySelector(".overtype-preview"),!this.textarea||!this.preview){this.container.remove(),this._buildFromScratch();return}this.wrapper._instance=this,this.options.fontSize&&this.wrapper.style.setProperty("--instance-font-size",this.options.fontSize),this.options.lineHeight&&this.wrapper.style.setProperty("--instance-line-height",String(this.options.lineHeight)),this.options.padding&&this.wrapper.style.setProperty("--instance-padding",this.options.padding),this._configureTextarea(),this._applyOptions()}_buildFromScratch(){let e=this._extractContent();this.element.innerHTML="",this._createDOM(),(e||this.options.value)&&this.setValue(e||this.options.value),this._applyOptions()}_extractContent(){let e=this.element.querySelector(".overtype-input");return e?e.value:this.element.textContent||""}_createDOM(){this.container=document.createElement("div"),this.container.className="overtype-container";let e=this.instanceTheme||v.currentTheme||_,n=typeof e=="string"?e:e.name;if(n&&this.container.setAttribute("data-theme",n),this.instanceTheme){let i=typeof this.instanceTheme=="string"?V(this.instanceTheme):this.instanceTheme;if(i&&i.colors){let o=Q(i.colors);this.container.style.cssText+=o}}this.wrapper=document.createElement("div"),this.wrapper.className="overtype-wrapper",this.options.fontSize&&this.wrapper.style.setProperty("--instance-font-size",this.options.fontSize),this.options.lineHeight&&this.wrapper.style.setProperty("--instance-line-height",String(this.options.lineHeight)),this.options.padding&&this.wrapper.style.setProperty("--instance-padding",this.options.padding),this.wrapper._instance=this,this.textarea=document.createElement("textarea"),this.textarea.className="overtype-input",this.textarea.placeholder=this.options.placeholder,this._configureTextarea(),this.options.textareaProps&&Object.entries(this.options.textareaProps).forEach(([i,o])=>{i==="className"||i==="class"?this.textarea.className+=" "+o:i==="style"&&typeof o=="object"?Object.assign(this.textarea.style,o):this.textarea.setAttribute(i,o)}),this.preview=document.createElement("div"),this.preview.className="overtype-preview",this.preview.setAttribute("aria-hidden","true"),this.placeholderEl=document.createElement("div"),this.placeholderEl.className="overtype-placeholder",this.placeholderEl.setAttribute("aria-hidden","true"),this.placeholderEl.textContent=this.options.placeholder,this.wrapper.appendChild(this.textarea),this.wrapper.appendChild(this.preview),this.wrapper.appendChild(this.placeholderEl),this.container.appendChild(this.wrapper),this.options.showStats&&(this.statsBar=document.createElement("div"),this.statsBar.className="overtype-stats",this.container.appendChild(this.statsBar),this._updateStats()),this.element.appendChild(this.container),this.options.autoResize?this._setupAutoResize():this.container.classList.remove("overtype-auto-resize")}_configureTextarea(){this.textarea.setAttribute("autocomplete","off"),this.textarea.setAttribute("autocorrect","off"),this.textarea.setAttribute("autocapitalize","off"),this.textarea.setAttribute("spellcheck",String(this.options.spellcheck)),this.textarea.setAttribute("data-gramm","false"),this.textarea.setAttribute("data-gramm_editor","false"),this.textarea.setAttribute("data-enable-grammarly","false")}_createToolbar(){var n;let e=this.options.toolbarButtons||ie;if((n=this.options.fileUpload)!=null&&n.enabled&&!e.some(i=>(i==null?void 0:i.name)==="upload")){let i=e.findIndex(o=>(o==null?void 0:o.name)==="viewMode");i!==-1?(e=[...e],e.splice(i,0,S.separator,S.upload)):e=[...e,S.separator,S.upload]}this.toolbar=new ke(this,{toolbarButtons:e}),this.toolbar.create(),this._toolbarSelectionListener=()=>{this.toolbar&&this.toolbar.updateButtonStates()},this._toolbarInputListener=()=>{this.toolbar&&this.toolbar.updateButtonStates()},this.textarea.addEventListener("selectionchange",this._toolbarSelectionListener),this.textarea.addEventListener("input",this._toolbarInputListener)}_cleanupToolbarListeners(){this._toolbarSelectionListener&&(this.textarea.removeEventListener("selectionchange",this._toolbarSelectionListener),this._toolbarSelectionListener=null),this._toolbarInputListener&&(this.textarea.removeEventListener("input",this._toolbarInputListener),this._toolbarInputListener=null)}_rebuildActionsMap(){var e;this.actionsById=ze(ie),this.options.toolbarButtons&&Object.assign(this.actionsById,ze(this.options.toolbarButtons)),(e=this.options.fileUpload)!=null&&e.enabled&&Object.assign(this.actionsById,ze([S.upload]))}_applyOptions(){this.options.autofocus&&this.textarea.focus(),this.options.autoResize?this.container.classList.contains("overtype-auto-resize")?this._updateAutoHeight():this._setupAutoResize():this.container.classList.remove("overtype-auto-resize"),this.options.toolbar&&!this.toolbar?this._createToolbar():!this.options.toolbar&&this.toolbar&&(this._cleanupToolbarListeners(),this.toolbar.destroy(),this.toolbar=null),this.placeholderEl&&(this.placeholderEl.textContent=this.options.placeholder),this.options.fileUpload&&!this.fileUploadInitialized?this._initFileUpload():!this.options.fileUpload&&this.fileUploadInitialized&&this._destroyFileUpload(),this.updatePreview()}_initFileUpload(){let e=this.options.fileUpload;if(!(!e||!e.enabled)){if(e.maxSize=e.maxSize||10*1024*1024,e.mimeTypes=e.mimeTypes||[],e.batch=e.batch||!1,!e.onInsertFile||typeof e.onInsertFile!="function"){console.warn("OverType: fileUpload.onInsertFile callback is required for file uploads.");return}this._fileUploadCounter=0,this._boundHandleFilePaste=this._handleFilePaste.bind(this),this._boundHandleFileDrop=this._handleFileDrop.bind(this),this._boundHandleDragOver=this._handleDragOver.bind(this),this.textarea.addEventListener("paste",this._boundHandleFilePaste),this.textarea.addEventListener("drop",this._boundHandleFileDrop),this.textarea.addEventListener("dragover",this._boundHandleDragOver),this.fileUploadInitialized=!0}}_handleFilePaste(e){var n,i;(i=(n=e==null?void 0:e.clipboardData)==null?void 0:n.files)!=null&&i.length&&(e.preventDefault(),this._handleDataTransfer(e.clipboardData))}_handleFileDrop(e){var n,i;(i=(n=e==null?void 0:e.dataTransfer)==null?void 0:n.files)!=null&&i.length&&(e.preventDefault(),this._handleDataTransfer(e.dataTransfer))}_handleDataTransfer(e){let n=[];for(let i of e.files){if(i.size>this.options.fileUpload.maxSize||this.options.fileUpload.mimeTypes.length>0&&!this.options.fileUpload.mimeTypes.includes(i.type))continue;let o=++this._fileUploadCounter,s=`${i.type.startsWith("image/")?"!":""}[Uploading ${i.name} (#${o})...]()`;if(this.insertAtCursor(`${s} -`),this.options.fileUpload.batch){n.push({file:i,placeholder:s});continue}this.options.fileUpload.onInsertFile(i).then(a=>{this.textarea.value=this.textarea.value.replace(s,a),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))},a=>{console.error("OverType: File upload failed",a),this.textarea.value=this.textarea.value.replace(s,"[Upload failed]()"),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))})}this.options.fileUpload.batch&&n.length>0&&this.options.fileUpload.onInsertFile(n.map(i=>i.file)).then(i=>{(Array.isArray(i)?i:[i]).forEach((r,s)=>{this.textarea.value=this.textarea.value.replace(n[s].placeholder,r)}),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))},i=>{console.error("OverType: File upload failed",i),n.forEach(({placeholder:o})=>{this.textarea.value=this.textarea.value.replace(o,"[Upload failed]()")}),this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))})}_handleDragOver(e){e.preventDefault()}_destroyFileUpload(){this.textarea.removeEventListener("paste",this._boundHandleFilePaste),this.textarea.removeEventListener("drop",this._boundHandleFileDrop),this.textarea.removeEventListener("dragover",this._boundHandleDragOver),this._boundHandleFilePaste=null,this._boundHandleFileDrop=null,this._boundHandleDragOver=null,this.fileUploadInitialized=!1}insertAtCursor(e){let n=this.textarea.selectionStart,i=this.textarea.selectionEnd,o=!1;try{o=document.execCommand("insertText",!1,e)}catch(r){}if(!o){let r=this.textarea.value.slice(0,n),s=this.textarea.value.slice(i);this.textarea.value=r+e+s,this.textarea.setSelectionRange(n+e.length,n+e.length)}this.textarea.dispatchEvent(new Event("input",{bubbles:!0}))}updatePreview(){let e=this.textarea.value,n=this.textarea.selectionStart,i=this._getCurrentLine(e,n),o=this.container.dataset.mode==="preview",r=T.parse(e,i,this.options.showActiveLineRaw,this.options.codeHighlighter,o);this.preview.innerHTML=r,this.placeholderEl&&(this.placeholderEl.style.display=e?"none":""),this._applyCodeBlockBackgrounds(),this.options.showStats&&this.statsBar&&this._updateStats(),this.options.onChange&&this.initialized&&this.options.onChange(e,this)}_applyCodeBlockBackgrounds(){let e=this.preview.querySelectorAll(".code-fence");for(let n=0;nd.replace(/^ /,"")).join(` -`);document.execCommand?(this.textarea.setSelectionRange(i,o),document.execCommand("insertText",!1,l)):(this.textarea.value=s+l+c,this.textarea.selectionStart=i,this.textarea.selectionEnd=i+l.length)}else if(i!==o){let s=r.substring(0,i),a=r.substring(i,o),c=r.substring(o),l=a.split(` -`).map(d=>" "+d).join(` -`);document.execCommand?(this.textarea.setSelectionRange(i,o),document.execCommand("insertText",!1,l)):(this.textarea.value=s+l+c,this.textarea.selectionStart=i,this.textarea.selectionEnd=i+l.length)}else document.execCommand?document.execCommand("insertText",!1," "):(this.textarea.value=r.substring(0,i)+" "+r.substring(o),this.textarea.selectionStart=this.textarea.selectionEnd=i+2);this.textarea.dispatchEvent(new Event("input",{bubbles:!0}));return}if(e.key==="Enter"&&!e.shiftKey&&!e.metaKey&&!e.ctrlKey&&this.options.smartLists&&this.handleSmartListContinuation()){e.preventDefault();return}!this.shortcuts.handleKeydown(e)&&this.options.onKeydown&&this.options.onKeydown(e,this)}handleSmartListContinuation(){let e=this.textarea,n=e.selectionStart,i=T.getListContext(e.value,n);return!i||!i.inList?!1:i.content.trim()===""&&n>=i.markerEndPos?(this.deleteListMarker(i),!0):(n>i.markerEndPos&&n{this.updateNumberedLists()},10)}updateNumberedLists(){let e=this.textarea.value,n=this.textarea.selectionStart,i=T.renumberLists(e);if(i!==e){let o=0,r=e.split(` -`),s=i.split(` -`),a=0;for(let p=0;pthis.getValue(),setValue:s=>this.setValue(s),event:n}),!0}catch(s){return console.error(`OverType: Action "${e}" error:`,s),this.wrapper.dispatchEvent(new CustomEvent("button-error",{detail:{actionId:e,error:s}})),!1}}getRenderedHTML(e={}){let n=this.getValue(),i=T.parse(n,-1,!1,this.options.codeHighlighter);return e.cleanHTML&&(i=i.replace(/.*?<\/span>/g,""),i=i.replace(/\sclass="(bullet-list|ordered-list|code-fence|hr-marker|blockquote|url-part)"/g,""),i=i.replace(/\sclass=""/g,"")),i}getPreviewHTML(){return this.preview.innerHTML}getCleanHTML(){return this.getRenderedHTML({cleanHTML:!0})}focus(){this.textarea.focus()}blur(){this.textarea.blur()}isInitialized(){return this.initialized}reinit(e={}){var o;let n=(o=this.options)==null?void 0:o.toolbarButtons;this.options=this._mergeOptions({...this.options,...e});let i=this.toolbar&&this.options.toolbar&&ri(n,this.options.toolbarButtons);this._rebuildActionsMap(),i&&(this._cleanupToolbarListeners(),this.toolbar.destroy(),this.toolbar=null,this._createToolbar()),this.fileUploadInitialized&&this._destroyFileUpload(),this.options.fileUpload&&this._initFileUpload(),this._applyOptions(),this.updatePreview()}showToolbar(){this.toolbar?this.toolbar.show():this._createToolbar()}hideToolbar(){this.toolbar&&this.toolbar.hide()}setTheme(e){if(v._autoInstances.delete(this),this.instanceTheme=e,e==="auto")v._autoInstances.add(this),v._startAutoListener(),this._applyResolvedTheme(Oe("auto"));else{let n=typeof e=="string"?V(e):e,i=typeof n=="string"?n:n.name;if(i&&this.container.setAttribute("data-theme",i),n&&n.colors){let o=Q(n.colors,n.previewColors);this.container.style.cssText+=o}this.updatePreview()}return v._stopAutoListener(),this}_applyResolvedTheme(e){let n=V(e);this.container.setAttribute("data-theme",e),n&&n.colors&&(this.container.style.cssText=Q(n.colors,n.previewColors)),this.updatePreview()}setCodeHighlighter(e){this.options.codeHighlighter=e,this.updatePreview()}_updateStats(){if(!this.statsBar)return;let e=this.textarea.value,n=e.split(` -`),i=e.length,o=e.split(/\s+/).filter(l=>l.length>0).length,r=this.textarea.selectionStart,a=e.substring(0,r).split(` -`),c=a.length,p=a[a.length-1].length+1;this.options.statsFormatter?this.statsBar.innerHTML=this.options.statsFormatter({chars:i,words:o,lines:n.length,line:c,column:p}):this.statsBar.innerHTML=` -
    - - ${i} chars, ${o} words, ${n.length} lines -
    -
    Line ${c}, Col ${p}
    - `}_setupAutoResize(){this.container.classList.add("overtype-auto-resize"),this.previousHeight=null,this._updateAutoHeight(),this.textarea.addEventListener("input",()=>this._updateAutoHeight()),window.addEventListener("resize",()=>this._updateAutoHeight())}_updateAutoHeight(){if(!this.options.autoResize)return;let e=this.textarea,n=this.preview,i=this.wrapper;if(this.container.dataset.mode==="preview"){i.style.removeProperty("height"),n.style.removeProperty("height"),n.style.removeProperty("overflow-y"),e.style.removeProperty("height"),e.style.removeProperty("overflow-y");return}let r=e.scrollTop;i.style.setProperty("height","auto","important"),e.style.setProperty("height","auto","important");let s=e.scrollHeight;if(this.options.minHeight){let p=parseInt(this.options.minHeight);s=Math.max(s,p)}let a="hidden";if(this.options.maxHeight){let p=parseInt(this.options.maxHeight);s>p&&(s=p,a="auto")}let c=s+"px";e.style.setProperty("height",c,"important"),e.style.setProperty("overflow-y",a,"important"),n.style.setProperty("height",c,"important"),n.style.setProperty("overflow-y",a,"important"),i.style.setProperty("height",c,"important"),e.scrollTop=r,n.scrollTop=r,this.previousHeight!==s&&(this.previousHeight=s)}showStats(e){this.options.showStats=e,e&&!this.statsBar?(this.statsBar=document.createElement("div"),this.statsBar.className="overtype-stats",this.container.appendChild(this.statsBar),this._updateStats()):e&&this.statsBar?this._updateStats():!e&&this.statsBar&&(this.statsBar.remove(),this.statsBar=null)}showNormalEditMode(){return this.container.dataset.mode="normal",this.updatePreview(),this._updateAutoHeight(),requestAnimationFrame(()=>{this.textarea.scrollTop=this.preview.scrollTop,this.textarea.scrollLeft=this.preview.scrollLeft}),this}showPlainTextarea(){if(this.container.dataset.mode="plain",this._updateAutoHeight(),this.toolbar){let e=this.container.querySelector('[data-action="toggle-plain"]');e&&(e.classList.remove("active"),e.title="Show markdown preview")}return this}showPreviewMode(){return this.container.dataset.mode="preview",this.updatePreview(),this._updateAutoHeight(),this}destroy(){if(v._autoInstances.delete(this),v._stopAutoListener(),this.fileUploadInitialized&&this._destroyFileUpload(),this.element.overTypeInstance=null,v.instances.delete(this.element),this.shortcuts&&this.shortcuts.destroy(),this.wrapper){let e=this.getValue();this.wrapper.remove(),this.element.textContent=e}this.initialized=!1}static init(e,n={}){return new v(e,n)}static initFromData(e,n={}){let i=document.querySelectorAll(e);return Array.from(i).map(o=>{let r={...n};for(let s of o.attributes)if(s.name.startsWith("data-ot-")){let c=s.name.slice(8).replace(/-([a-z])/g,(p,l)=>l.toUpperCase());r[c]=v._parseDataValue(s.value)}return new v(o,r)[0]})}static _parseDataValue(e){return e==="true"?!0:e==="false"?!1:e==="null"?null:e!==""&&!isNaN(Number(e))?Number(e):e}static getInstance(e){return e.overTypeInstance||v.instances.get(e)||null}static destroyAll(){document.querySelectorAll("[data-overtype-instance]").forEach(n=>{let i=v.getInstance(n);i&&i.destroy()})}static injectStyles(e=!1){if(v.stylesInjected&&!e)return;let n=document.querySelector("style.overtype-styles");n&&n.remove();let i=v.currentTheme||_,o=Ze({theme:i}),r=document.createElement("style");r.className="overtype-styles",r.textContent=o,document.head.appendChild(r),v.stylesInjected=!0}static setTheme(e,n=null){if(v._globalAutoTheme=!1,v._globalAutoCustomColors=null,e==="auto"){v._globalAutoTheme=!0,v._globalAutoCustomColors=n,v._startAutoListener(),v._applyGlobalTheme(Oe("auto"),n);return}v._stopAutoListener(),v._applyGlobalTheme(e,n)}static _applyGlobalTheme(e,n=null){let i=typeof e=="string"?V(e):e;n&&(i=Ke(i,n)),v.currentTheme=i,v.injectStyles(!0);let o=typeof i=="string"?i:i.name;document.querySelectorAll(".overtype-container").forEach(r=>{o&&r.setAttribute("data-theme",o)}),document.querySelectorAll(".overtype-wrapper").forEach(r=>{r.closest(".overtype-container")||o&&r.setAttribute("data-theme",o);let s=r._instance;s&&s.updatePreview()}),document.querySelectorAll("overtype-editor").forEach(r=>{o&&typeof r.setAttribute=="function"&&r.setAttribute("theme",o),typeof r.refreshTheme=="function"&&r.refreshTheme()})}static _startAutoListener(){v._autoMediaQuery||window.matchMedia&&(v._autoMediaQuery=window.matchMedia("(prefers-color-scheme: dark)"),v._autoMediaListener=e=>{let n=e.matches?"cave":"solar";v._globalAutoTheme&&v._applyGlobalTheme(n,v._globalAutoCustomColors),v._autoInstances.forEach(i=>i._applyResolvedTheme(n))},v._autoMediaQuery.addEventListener("change",v._autoMediaListener))}static _stopAutoListener(){v._autoInstances.size>0||v._globalAutoTheme||v._autoMediaQuery&&(v._autoMediaQuery.removeEventListener("change",v._autoMediaListener),v._autoMediaQuery=null,v._autoMediaListener=null)}static setCodeHighlighter(e){T.setCodeHighlighter(e),document.querySelectorAll(".overtype-wrapper").forEach(n=>{let i=n._instance;i&&i.updatePreview&&i.updatePreview()}),document.querySelectorAll("overtype-editor").forEach(n=>{if(typeof n.getEditor=="function"){let i=n.getEditor();i&&i.updatePreview&&i.updatePreview()}})}static setCustomSyntax(e){T.setCustomSyntax(e),document.querySelectorAll(".overtype-wrapper").forEach(n=>{let i=n._instance;i&&i.updatePreview&&i.updatePreview()}),document.querySelectorAll("overtype-editor").forEach(n=>{if(typeof n.getEditor=="function"){let i=n.getEditor();i&&i.updatePreview&&i.updatePreview()}})}static initGlobalListeners(){v.globalListenersInitialized||(document.addEventListener("input",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),i=n==null?void 0:n._instance;i&&i.handleInput(e)}}),document.addEventListener("keydown",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),i=n==null?void 0:n._instance;i&&i.handleKeydown(e)}}),document.addEventListener("scroll",e=>{if(e.target&&e.target.classList&&e.target.classList.contains("overtype-input")){let n=e.target.closest(".overtype-wrapper"),i=n==null?void 0:n._instance;i&&i.handleScroll(e)}},!0),document.addEventListener("selectionchange",e=>{let n=document.activeElement;if(n&&n.classList.contains("overtype-input")){let i=n.closest(".overtype-wrapper"),o=i==null?void 0:i._instance;o&&(o.options.showStats&&o.statsBar&&o._updateStats(),clearTimeout(o._selectionTimeout),o._selectionTimeout=setTimeout(()=>{o.updatePreview()},50))}}),v.globalListenersInitialized=!0)}};C(v,"instances",new WeakMap),C(v,"stylesInjected",!1),C(v,"globalListenersInitialized",!1),C(v,"instanceCount",0),C(v,"_autoMediaQuery",null),C(v,"_autoMediaListener",null),C(v,"_autoInstances",new Set),C(v,"_globalAutoTheme",!1),C(v,"_globalAutoCustomColors",null);var z=v;z.MarkdownParser=T;z.ShortcutsManager=re;z.themes={solar:_,cave:V("cave")};z.getTheme=V;z.currentTheme=_;var si=z;return mn(ai);})(); -/** - * OverType - A lightweight markdown editor library with perfect WYSIWYG alignment - * @version 1.0.0 - * @license MIT - */ - -if (typeof window !== "undefined" && typeof window.document !== "undefined") { - // Extract exports BEFORE reassigning OverType (var OverType is window.OverType) - window.toolbarButtons = OverType.toolbarButtons; - window.defaultToolbarButtons = OverType.defaultToolbarButtons; - window.OverType = OverType.default ? OverType.default : OverType; -} - diff --git a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/MarkdownUtils.js b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/MarkdownUtils.js index c245296bb3..4bcbcff0da 100644 --- a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/MarkdownUtils.js +++ b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/MarkdownUtils.js @@ -1,6 +1,6 @@ import "../../lib/DOMPurify/3.3.1/DOMPurify.min.js"; import "../../lib/Marked/17.0.1/marked.min.js"; -import "../../lib/overtype/2.3.4/overtype.min.js"; +import "../../lib/overtype/2.3.10/overtype.min.js"; /** * https://marked.js.org/ diff --git a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemAddEdit.js b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemAddEdit.js index 9c8a8ba9ad..2488388bca 100644 --- a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemAddEdit.js +++ b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemAddEdit.js @@ -205,10 +205,11 @@ export class ItemAddEdit extends PageUtility { break; } - data.storageBlocks.forEach(curStorageBlockId => { + data.storageBlocks.forEach(curStorageBlockSettings => { + let curStorageBlockId = curStorageBlockSettings.storageBlock; Getters.StorageBlock.getStorageBlockLabel(curStorageBlockId, function (label) { //TODO:: determine if we are allowed to remove (if has stored items in it or not) - ItemAddEdit.storageInput.addStorage(label, curStorageBlockId); + ItemAddEdit.storageInput.addStorage(label, curStorageBlockId, curStorageBlockSettings); }); }); @@ -240,10 +241,118 @@ export class ItemAddEdit extends PageUtility { } static storageInput = class { - static addStorage(blockName, blockId) { + static storageBlockInputCount = 0; + + static updateStorageInputAdvancedVisibility(storageInput) { + console.log("Updating storage input advanced inputs visibility."); + let advancedToggle = storageInput.find(".form-check-input"); + let advancedDiv = storageInput.find(".advancedInputs"); + if (advancedToggle.is(":checked")) { + advancedDiv.show(); + } else { + advancedDiv.hide(); + } + } + static Getters = class { + static blockIdInput(blockInput){ + return blockInput.find("input[name='storageBlock']"); + } + static getStorageBlockIds(){ + return ItemAddEdit.associatedStorageInputContainer.find("input[name='storageBlock']"); + } + static getStorageBlockInputs(){ + return ItemAddEdit.associatedStorageInputContainer.find(".blockSelection"); + } + static lowStockSettingUnitInput(blockInput){ + return blockInput.find(".storedSettingLowStockThresholdUnit"); + } + static lowStockSettingValueInput(blockInput){ + return blockInput.find(".storedSettingLowStockThresholdValue"); + } + static notesSettingInput(blockInput){ + return blockInput.find(".storedSettingNotesInput"); + } + static notesSettingField(blockInput){ + return blockInput.data("notesField"); + } + } + + + static newStorageInput(blockId, blockName, settings) { + let inputNum = ItemAddEdit.storageInput.storageBlockInputCount++; + let newBlock = $(` +
    + +
    +
    +

    +
    + +
    +

    + Low Stock Threshold: +

    +
    + + +
    + +

    + Notes: +

    +
    +
    +
    +
    + `); + + let notesField = MarkdownUtils.Editor.initInput(ItemAddEdit.storageInput.Getters.notesSettingInput(newBlock))[0]; + newBlock.data("notesField", notesField); + + UnitUtils.getCompatibleUnitOptions(ItemAddEdit.getUnit()) + .then(function (options) { + ItemAddEdit.storageInput.Getters.lowStockSettingUnitInput(newBlock).html(options); + if(settings.lowStockThreshold){ + ItemAddEdit.storageInput.Getters.lowStockSettingUnitInput(newBlock).val(settings.lowStockThreshold.unit.string) + } + }); + + newBlock.attr("data-block-id", blockId); + ItemAddEdit.storageInput.Getters.blockIdInput(newBlock).val(blockId); + newBlock.find(".blockInputName").text(blockName); + //TODO:: image + + + if(settings){ + if(settings.hasSettings){ + newBlock.find(".storageBlockInputAdvancedSettingsToggle").prop("checked", true); + } + if(settings.notes){ + ItemAddEdit.storageInput.Getters.notesSettingField(newBlock).setValue(settings.notes); + } + if(settings.lowStockThreshold){ + ItemAddEdit.storageInput.Getters.lowStockSettingValueInput(newBlock).val(settings.lowStockThreshold.value) + } + } + + ItemAddEdit.storageInput.updateStorageInputAdvancedVisibility(newBlock); + + return newBlock; + } + + static addStorage(blockName, blockId, storageSettings = null) { Main.processStart(); let found = false; - ItemAddEdit.associatedStorageInputContainer.find('input[name="storageBlocks[]"]').each(function () { + ItemAddEdit.storageInput.Getters.getStorageBlockIds().each(function () { if ($(this).val() === blockId) { found = true; } @@ -253,22 +362,9 @@ export class ItemAddEdit extends PageUtility { return; } - let newBlock = $('
    ' + - ' ' + - '
    ' + - '
    ' + - '

    ' + - '
    ' + - ' ' + - '
    ' + - '
    '); - newBlock.attr("data-block-id", blockId); - newBlock.find('input[name="storageBlocks[]"]').val(blockId); - newBlock.find(".blockInputName").text(blockName); - - ItemAddEdit.associatedStorageInputContainer.append(newBlock); + ItemAddEdit.associatedStorageInputContainer.append( + ItemAddEdit.storageInput.newStorageInput(blockId, blockName, storageSettings) + ); Main.processStop(); } static removeStorage(removeButtonClicked) {//or input card? @@ -280,10 +376,19 @@ export class ItemAddEdit extends PageUtility { } } static selectedStorageList() { - return ItemAddEdit.associatedStorageInputContainer.find("input[name='storageBlocks[]']") + return ItemAddEdit.storageInput.Getters.getStorageBlockInputs()//TODO:: update to do advanced fields .map(function () { - return $(this).val(); - }).get(); + let input = $(this); + return { + "storageBlock": ItemAddEdit.storageInput.Getters.blockIdInput(input).val(), + "notes": ItemAddEdit.storageInput.Getters.notesSettingField(input).getValue(), + "lowStockThreshold": (ItemAddEdit.storageInput.Getters.lowStockSettingValueInput(input).val() ? UnitUtils.getQuantityObj( + ItemAddEdit.storageInput.Getters.lowStockSettingValueInput(input).val(), + ItemAddEdit.storageInput.Getters.lowStockSettingUnitInput(input).val() + ) : null) + } + }) + .get(); } } static getUnit(force = false){ @@ -297,12 +402,13 @@ export class ItemAddEdit extends PageUtility { console.log("Item Unit Changed to ", itemUnit); let lowStockUnitPromise = ItemAddEdit.updateLowStockUnits(itemUnit, force); + let storageBlocksSettingsLowStockUnitsPromise = ItemAddEdit.storageBlockSettingsLowStockUnits(itemUnit, force); let pricingUnitPromise = Pricing.setUnit( ItemAddEdit.addEditItemPricingInput, itemUnit ); - await Promise.all([lowStockUnitPromise, pricingUnitPromise]); + await Promise.all([lowStockUnitPromise, storageBlocksSettingsLowStockUnitsPromise, pricingUnitPromise]); } static updateLowStockUnits(itemUnit, force = false) { return UnitUtils.getCompatibleUnitOptions(itemUnit) @@ -310,6 +416,15 @@ export class ItemAddEdit extends PageUtility { ItemAddEdit.addEditItemTotalLowStockThresholdUnitInput.html(options); }); } + static storageBlockSettingsLowStockUnits(itemUnit, force = false) { + return UnitUtils.getCompatibleUnitOptions(itemUnit) + .then(function (options) { + ItemAddEdit.storageInput.Getters.getStorageBlockInputs().each(function (i, blockInput) { + let opsCopy = options.clone(); + ItemAddEdit.storageInput.Getters.lowStockSettingUnitInput($(blockInput)).html(opsCopy); + }); + }); + } static { window.ItemAddEdit = this; @@ -364,7 +479,7 @@ export class ItemAddEdit extends PageUtility { KeywordAttEdit.addKeywordAttData(addEditData, ItemAddEdit.addEditKeywordDiv, ItemAddEdit.addEditAttDiv); ImageSearchSelect.addImagesToData(addEditData, ItemAddEdit.addEditItemImagesSelected); - console.log("Data being submitted: " + JSON.stringify(addEditData)); + console.log("Data being submitted: ", addEditData); let verb = ""; let result = false; if (ItemAddEdit.addEditItemFormMode.val() === "add") { @@ -376,7 +491,7 @@ export class ItemAddEdit extends PageUtility { data: addEditData, async: false, done: function (data) { - console.log("Response from create request: " + JSON.stringify(data)); + console.log("Response from create request: ", data); result = true; }, failMessagesDiv: ItemAddEdit.addEditItemFormMessages @@ -392,7 +507,7 @@ export class ItemAddEdit extends PageUtility { data: addEditData, async: false, done: function (data) { - console.log("Response from create request: " + JSON.stringify(data)); + console.log("Response from create request: ", data); result = true; }, failMessagesDiv: ItemAddEdit.addEditItemFormMessages diff --git a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemView.js b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemView.js index ab272c66d3..161d29e867 100644 --- a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemView.js +++ b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/ItemView.js @@ -224,7 +224,12 @@ export class ItemView extends PageUtility { labelText.text(blockLabel); //TODO:: image - //TODO:: stats + + $("") + .text(" / " + item.stats.storageBlockStats[blockId].total.value + "" + item.stats.storageBlockStats[blockId].total.unit.symbol) + .appendTo(labelText); + + ItemView.getBlockFlags(item, blockId).appendTo(labelText); collapseButton.empty(); collapseButton.append($(Icons.storageBlock).addClass("me-2")); @@ -269,6 +274,7 @@ export class ItemView extends PageUtility { } static getMultiStoredInBlockView (itemData, blockId) { + let blockSettings = itemData.storageBlocks.find(function (blockSettings) {return blockSettings.storageBlock === blockId;}); let output = $('
    '); let dataRow = $('
    '); @@ -285,6 +291,16 @@ export class ItemView extends PageUtility { UnitUtils.quantityToDisplayStr(itemData.stats.storageBlockStats[blockId].total) ) ) + .append( + blockSettings.lowStockThreshold ? + $('
    ') + .append($(`

    Low Stock Threshold:

    `)) + .append( + $(`

    `) + .text(blockSettings.lowStockThreshold.value + "" + blockSettings.lowStockThreshold.unit.symbol) + ) + : "" + ) ) ); if(itemData.stats.storageBlockStats[blockId].prices.length){ @@ -316,11 +332,101 @@ export class ItemView extends PageUtility { dataRow.append(pricesAccord); } + if(blockSettings.notes){ + dataRow.append( + $('
    ') + .append( + $('
    ') + .append($('
    Notes:
    ')) + .append(MarkdownUtils.Parsing.parseMarkdown(blockSettings.notes)) + ) + ); + } output.append(ItemView.getStoredInBlockSearch(itemData.id, blockId)); return output; } + + static getBlockFlags(itemData, blockId) { + let blockSettings = itemData.storageBlocks.find(function (blockSettings) {return blockSettings.storageBlock === blockId;}); + let blockStats = itemData.stats.storageBlockStats[blockId]; + + let output = $(''); + + if(blockStats.anyLowStock) { + let lowStockBadge = $(''); + + if (blockStats.lowStock) { + lowStockBadge.append( + $(Icons.storageBlock) + ); + } + if (blockStats.hasLowStockStored) { + lowStockBadge.append( + $(Icons.stored) + ); + } + + lowStockBadge.append($(Icons.lowStock)); + output.append(lowStockBadge); + } + + if(blockStats.numExpiryWarn) { + output.append( + $(''+Icons.expiring+'') + ); + } + if(blockStats.numExpired) { + output.append( + $(''+Icons.expired+'') + ); + } + + return output; + + } + + static getItemFlags(itemData) { + let output = $(''); + + if(itemData.stats.anyLowStock) { + let lowStockBadge = $(''); + + if (itemData.stats.lowStock) { + lowStockBadge.append( + $(Icons.item) + ); + } + if (itemData.stats.hasLowStockInBlock) { + lowStockBadge.append( + $(Icons.storageBlock) + ); + } + if (itemData.stats.numLowStock) { + lowStockBadge.append( + $(Icons.stored) + ); + } + + lowStockBadge.append($(Icons.lowStock)); + output.append(lowStockBadge); + } + + if(itemData.stats.numExpiryWarn) { + output.append( + $(''+Icons.expiring+'') + ); + } + if(itemData.stats.numExpired) { + output.append( + $(''+Icons.expired+'') + ); + } + + return output; + } + static setupView(itemId) { Main.processStart(); console.log("Setting up view for item " + itemId); @@ -377,8 +483,8 @@ export class ItemView extends PageUtility { 1000 ); - - itemData.storageBlocks.forEach(function (blockId) { + itemData.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; console.debug("Displaying block: ", blockId); if (itemData.stats.storageBlockStats[blockId].numStored) { @@ -413,7 +519,8 @@ export class ItemView extends PageUtility { StorageTypeUtils.runForType( itemData, function () { - itemData.storageBlocks.forEach(function (blockId) { + itemData.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; console.debug("Displaying block: ", blockId); if (itemData.stats.storageBlockStats[blockId].hasStored) { @@ -464,7 +571,8 @@ export class ItemView extends PageUtility { `); let alsoInLabel = storageLabel.find(".uniqueItemStoredAlsoInLabel"); - itemData.storageBlocks.forEach(function (curBlock) { + itemData.storageBlocks.forEach(function (blockSettings) { + let curBlock = blockSettings.storageBlock; promises.push(Getters.StorageBlock.getStorageBlockLabel(curBlock, function (labelText) { let newLink = Links.getStorageViewLink(curBlock, labelText); @@ -498,7 +606,8 @@ export class ItemView extends PageUtility { ItemView.storedNonePresentContainer.show(); if (itemData.storageBlocks.length) { - itemData.storageBlocks.forEach(function (curBlock) { + itemData.storageBlocks.forEach(function (blockSettings) { + let curBlock = blockSettings.storageBlock; Getters.StorageBlock.getStorageBlockLabel(curBlock, function (labelText) { let newLink = Links.getStorageViewLink(curBlock, labelText); ItemView.storedNonePresentBlocksList.append(newLink); @@ -534,9 +643,11 @@ export class ItemView extends PageUtility { ItemView.linksContainer.show(); } + ItemView.itemViewModalLabel.text(itemData.name); + ItemView.itemViewModalLabel.append(ItemView.getItemFlags(itemData)); + KeywordAttUtils.processKeywordDisplay(ItemView.viewKeywordsSection, itemData.keywords); KeywordAttUtils.processAttDisplay(ItemView.viewAttsSection, itemData.attributes); - ItemView.itemViewModalLabel.text(itemData.name); ItemView.itemViewStorageType.text(StorageTypeUtils.typeToDisplay(itemData.storageType)); ItemView.itemViewTotal.text(itemData.stats.total.value + "" + itemData.stats.total.unit.symbol); ItemView.itemViewTotalVal.text(itemData.valueOfStored);//TODO @@ -648,4 +759,4 @@ export class ItemView extends PageUtility { } }); } -}; +} diff --git a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/itemStored/ItemStoredTransaction.js b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/itemStored/ItemStoredTransaction.js index f349378ef1..73b2cc6647 100644 --- a/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/itemStored/ItemStoredTransaction.js +++ b/software/core/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/itemStored/ItemStoredTransaction.js @@ -234,7 +234,8 @@ export class ItemStoredTransaction extends PageUtility { ItemStoredTransaction.Add.itemNameInput.val(item.name); ItemStoredTransaction.Add.itemDisplayName.text(item.name); - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let blockOp = $(""); blockOp.val(blockId); blockOp.text(blockId); @@ -491,7 +492,8 @@ export class ItemStoredTransaction extends PageUtility { } //Setup Storage blocks in select - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let newBlockOption = $(''); newBlockOption.val(blockId); Getters.StorageBlock.getStorageBlockLabel(blockId, function (blockLabel) { @@ -528,7 +530,7 @@ export class ItemStoredTransaction extends PageUtility { let originalOption = ItemStoredTransaction.Checkin.toSelectInput.find("option[value='original']"); let blockOriginal = function () { - if (item.storageBlocks.includes(checkout.fromBlock)) { + if (item.storageBlocks.some(blockSetting => blockSetting.storageBlock === checkout.fromBlock)) { originalOption.prop("disabled", false); Getters.StorageBlock.getStorageBlockLabel(checkout.fromBlock, function (blockLabel) { ItemStoredTransaction.Checkin.toOrigDesc.html( @@ -888,7 +890,8 @@ export class ItemStoredTransaction extends PageUtility { if (fromBlock) { console.debug("Showing from block"); ItemStoredTransaction.Checkout.fromBlockContainer.show(); - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let newBlockOption = $(''); newBlockOption.val(blockId); promises.push(Getters.StorageBlock.getStorageBlockLabel(blockId, function (blockLabel) { @@ -1187,7 +1190,8 @@ export class ItemStoredTransaction extends PageUtility { function () { ItemStoredTransaction.Set.setTypeInput.val("block"); ItemStoredTransaction.Set.blockContainer.show(); - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let newBlockOption = $(''); newBlockOption.val(blockId); if (stored && stored.state.storageBlock === blockId) { @@ -1432,7 +1436,8 @@ export class ItemStoredTransaction extends PageUtility { if (fromBlock) { console.debug("Showing from block"); ItemStoredTransaction.Subtract.fromBlockContainer.show(); - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let newBlockOption = $(''); newBlockOption.val(blockId); promises.push(Getters.StorageBlock.getStorageBlockLabel(blockId, function (blockLabel) { @@ -1744,7 +1749,8 @@ export class ItemStoredTransaction extends PageUtility { if (fromBlock) { console.debug("Showing from block"); ItemStoredTransaction.Transfer.fromBlockContainer.show(); - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let newBlockOption = $(''); newBlockOption.val(blockId); promises.push(Getters.StorageBlock.getStorageBlockLabel(blockId, function (blockLabel) { @@ -1770,7 +1776,8 @@ export class ItemStoredTransaction extends PageUtility { if (toBlock) { console.debug("Showing to block"); ItemStoredTransaction.Transfer.toBlockContainer.show(); - item.storageBlocks.forEach(function (blockId) { + item.storageBlocks.forEach(function (blockSettings) { + let blockId = blockSettings.storageBlock; let newBlockOption = $(''); newBlockOption.val(blockId); promises.push(Getters.StorageBlock.getStorageBlockLabel(blockId, function (blockLabel) { diff --git a/software/core/oqm-core-base-station/src/main/resources/templates/tags/objView/itemViewModal.html b/software/core/oqm-core-base-station/src/main/resources/templates/tags/objView/itemViewModal.html index 0c9e1c3f07..718c8d32c3 100644 --- a/software/core/oqm-core-base-station/src/main/resources/templates/tags/objView/itemViewModal.html +++ b/software/core/oqm-core-base-station/src/main/resources/templates/tags/objView/itemViewModal.html @@ -2,7 +2,7 @@ {#titleIcon}{#icons/item /}{/titleIcon} {#footerButtons} - + @@ -50,13 +50,10 @@
    Categories:
    Total Held:

    -
    -
    -
    -
    -
    Total Low Stock Threshold:
    -

    -

    +
    +

    Low Stock Threshold:

    +

    +
    @@ -296,7 +293,7 @@

    - + {!

    !} {!!} {!
    !} @@ -313,4 +310,4 @@

    {#objView/history/objHistoryView containerId='itemHistory' objectUrl=(rootPrefix + '/api/passthrough/inventory/item/')}{/objView/history/objHistoryView}

  • -{/modal} \ No newline at end of file +{/modal} diff --git a/software/core/oqm-core-base-station/src/main/resources/templates/tags/object/inventoryItems/flags.qute.html b/software/core/oqm-core-base-station/src/main/resources/templates/tags/object/inventoryItems/flags.qute.html new file mode 100644 index 0000000000..79dd09f685 --- /dev/null +++ b/software/core/oqm-core-base-station/src/main/resources/templates/tags/object/inventoryItems/flags.qute.html @@ -0,0 +1,25 @@ +{#if item.get("stats").get("anyLowStock").asBoolean()} + + {#if item.get("stats").get("lowStock").asBoolean()} + {#icons/item /} + {/if} + {#if item.get("stats").get("hasLowStockInBlock").asBoolean()} + {#icons/storageBlock /} + {/if} + {#if item.get("stats").get("numLowStock").asInt() > 0} + {#icons/stored /} + {/if} + + {#icons/lowStock /} + +{/if} +{#if item.get("stats").get("numExpiryWarn").asInt() > 0} + + {#icons/expiring /} + +{/if} +{#if item.get("stats").get("numExpired").asInt() > 0} + + {#icons/expired /} + +{/if} diff --git a/software/core/oqm-core-base-station/src/main/resources/templates/tags/search/item/itemSearchResults.html b/software/core/oqm-core-base-station/src/main/resources/templates/tags/search/item/itemSearchResults.html index 574714c1b4..7829e9c2c2 100644 --- a/software/core/oqm-core-base-station/src/main/resources/templates/tags/search/item/itemSearchResults.html +++ b/software/core/oqm-core-base-station/src/main/resources/templates/tags/search/item/itemSearchResults.html @@ -74,21 +74,7 @@

    {result.get("stats").get("numStored").asInt()} - {#if result.get("stats").get("anyLowStock").asBoolean()} - - {#icons/lowStock /} - - {/if} - {#if result.get("stats").get("numExpiryWarn").asInt() > 0} - - {#icons/expiring /} - - {/if} - {#if result.get("stats").get("numExpired").asInt() > 0} - - {#icons/expired /} - - {/if} + {#object/inventoryItems/flags item=result /} {#if actionType == 'full'} @@ -114,7 +100,7 @@

    {#icons/view /} View {!{#itemStored/transaction/buttons/transactionDropdown dropdownClasses="btn-sm me-1" buttonText=false itemId=id checkinButton=false /}!} {/if} - + {/let} diff --git a/software/core/oqm-core-base-station/src/main/resources/templates/webui/js/Icons.js b/software/core/oqm-core-base-station/src/main/resources/templates/webui/js/Icons.js index 1c38f8c3bc..3d0cd22eb6 100644 --- a/software/core/oqm-core-base-station/src/main/resources/templates/webui/js/Icons.js +++ b/software/core/oqm-core-base-station/src/main/resources/templates/webui/js/Icons.js @@ -9,11 +9,14 @@ export class Icons extends PageUtility { static itemCheckout = ''; static itemCheckouts = '{#icons/itemCheckouts /}'; static edit = '{#icons/edit /}'; + static expiring = '{#icons/expiring /}'; + static expired = '{#icons/expired /}'; static info = '{#icons/info /}'; static link = '{#icons/link /}'; static locked = '{#icons/locked /}' static item = '{#icons/item /}'; static items = '{#icons/items /}'; + static lowStock = '{#icons/lowStock /}'; static pricing = '{#icons/pricing /}'; static stored = '{#icons/stored /}'; static newTab = '{#icons/newTab /}'; @@ -38,7 +41,7 @@ export class Icons extends PageUtility { static user = '{#icons/user /}'; static extService = '{#icons/extService /}'; static coreApi = '{#icons/coreApi /}'; - + static iconWithSub(icon, subIcon){ return icon + '' + subIcon + ''; } @@ -47,4 +50,4 @@ export class Icons extends PageUtility { window.Icons = this; console.log(this.name + " done initializing."); } -} \ No newline at end of file +} diff --git a/software/core/oqm-core-base-station/src/main/resources/templates/webui/mainWebPageTemplate.html b/software/core/oqm-core-base-station/src/main/resources/templates/webui/mainWebPageTemplate.html index 37aa328cf6..5a45cf5640 100644 --- a/software/core/oqm-core-base-station/src/main/resources/templates/webui/mainWebPageTemplate.html +++ b/software/core/oqm-core-base-station/src/main/resources/templates/webui/mainWebPageTemplate.html @@ -49,7 +49,7 @@
    {#if characteristics.getCharacteristics().getRunBy().hasLogoImg()} - Run By Logo + Run By Logo {/if} @@ -188,15 +188,15 @@ @@ -204,36 +204,59 @@
  • + +
  • +
  • + +
  • +
  • + + {#icons/icon icon='door-closed' /} Logout + +
  • +
    diff --git a/software/libs/core-api-lib-quarkus/deployment/pom.xml b/software/libs/core-api-lib-quarkus/deployment/pom.xml index 2b063627e0..d065b10819 100644 --- a/software/libs/core-api-lib-quarkus/deployment/pom.xml +++ b/software/libs/core-api-lib-quarkus/deployment/pom.xml @@ -8,7 +8,7 @@ tech.epic-breakfast-productions.openquartermaster.lib.core core-api-lib-quarkus-parent - 5.0.0 + 6.0.0-SNAPSHOT core-api-lib-quarkus-deployment Core Api Lib Quarkus - Deployment diff --git a/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/CoreApiLibQuarkusProcessor.java b/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/CoreApiLibQuarkusProcessor.java index 0a58849bcc..449f7aa6af 100644 --- a/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/CoreApiLibQuarkusProcessor.java +++ b/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/CoreApiLibQuarkusProcessor.java @@ -10,11 +10,13 @@ import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.dev.devservices.DevServicesConfig; +import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.Page; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import org.jboss.logging.Logger; +import org.testcontainers.Testcontainers; import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.DockerImageName; @@ -33,25 +35,25 @@ * Processes runtime features like configs, health checks, and devservices */ class CoreApiLibQuarkusProcessor { - + private static final Logger log = Logger.getLogger(CoreApiLibQuarkusProcessor.class); - + private static final String FEATURE = "core-api-lib-quarkus"; - private static final String MONGODB_DEVSERVICE_HOSTNAME = "localhost"; - private static final String HOST = "localhost"; - private static final String KEYCLOAK_DEVSERVICE_HOSTNAME = HOST; - private static final String KAFKA_DEVSERVICE_HOSTNAME = HOST; - + private static final String MONGODB_DEVSERVICE_HOSTNAME = "oqm-core-api-mongodb"; + private static final String HOST = "host.testcontainers.internal"; + private static final String KEYCLOAK_DEVSERVICE_HOSTNAME = "localhost"; //TODO: #1287 should not use this in non-host netowrking + private static final String KAFKA_DEVSERVICE_HOSTNAME = "localhost"; //TODO: #1287 should not use this in non-host netowrking + private static volatile boolean firstSetup = true; - + private static volatile Map DEVSERVICES = new HashMap<>(); - - + + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } - + @BuildStep List addRestConfiguration() { return List.of( @@ -59,25 +61,27 @@ List addRestConfiguration() { new RunTimeConfigurationDefaultBuildItem("quarkus.rest-client.\"" + Constants.CORE_API_CLIENT_OIDC_NAME + "\".url", "${quarkus.oidc.auth-server-url:}") ); } - + @BuildStep HealthBuildItem addHealthCheck(CoreApiLibBuildTimeConfig buildTimeConfig) { return new HealthBuildItem("tech.ebp.oqm.lib.core.api.quarkus.runtime.health.CoreApiHealthCheck", buildTimeConfig.health().enabled()); } - + private MongoDBContainer newMongoDbContainer() { log.info("Starting new MongoDB dev container"); DockerImageName mongoImageName = DockerImageName.parse("mongo:7"); - + MongoDBContainer mongoDBContainer = new MongoDBContainer(mongoImageName); mongoDBContainer.addExposedPorts(); - mongoDBContainer.withNetwork(Network.SHARED); -// mongoDBContainer.withNetworkAliases(MONGODB_DEVSERVICE_HOSTNAME); + + ConfigureUtil.configureSharedNetwork(mongoDBContainer, "oqm-core-api-mongodb"); + + mongoDBContainer.withNetworkAliases(MONGODB_DEVSERVICE_HOSTNAME); mongoDBContainer.start(); - + return mongoDBContainer; } - + private OqmCoreApiWebServiceContainer newCoreApiContainer( CoreApiLibBuildTimeConfig config, Map mongoConnectionInfo, @@ -90,7 +94,7 @@ private OqmCoreApiWebServiceContainer newCoreApiContainer( // .withAccessToHost(true) // .withNetwork(Network.SHARED) ; - + container.withEnv( "smallrye.jwt.verify.key.location", String.format( @@ -100,45 +104,60 @@ private OqmCoreApiWebServiceContainer newCoreApiContainer( config.devservices().keycloak().realm() ) ); - + container.start(); - + return container; } - - + + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = DevServicesConfig.Enabled.class) public List createContainer(LaunchModeBuildItem launchMode, CoreApiLibBuildTimeConfig config, CuratedApplicationShutdownBuildItem closeBuildItem) { log.info("Setting up OQM Core API related dev services."); + + //TODO:: #1287 these lines are related to not host netowrking +// Testcontainers.exposeHostPorts( +// config.devservices().keycloak().port(), +// config.devservices().kafka().port() +// ); + //TODO:: handle needing to restart services? List output = new ArrayList<>(); Map mongoConnectionInfo = new HashMap<>(); Map kafkaConnectionInfo = new HashMap<>(); {//mongodb - + DevServicesResultBuildItem.RunningDevService mongoDevService = DEVSERVICES.get("mongodb"); - + if (mongoDevService == null) { MongoDBContainer mongoDBContainer = newMongoDbContainer(); + + log.info("MongoDB dev service network aliases: " + mongoDBContainer.getNetworkAliases()); + + Map props = Map.of( + "host", + "localhost", //mongoDBContainer.getNetworkAliases().get(0), //TODO:: #1287 + "port", + String.valueOf(mongoDBContainer.getMappedPort(27017))//TODO:: #1287 + ); + + log.info("MongoDB dev service properties: {}" + props); + mongoDevService = new DevServicesResultBuildItem.RunningDevService( FEATURE, mongoDBContainer.getContainerId(), mongoDBContainer::close, - Map.of( - "port", - String.valueOf(mongoDBContainer.getMappedPort(27017)) - ) + props ); - - + DEVSERVICES.put("mongodb", mongoDevService); } - + mongoConnectionInfo.put( "quarkus.mongodb.connection-string", - "mongodb://" + MONGODB_DEVSERVICE_HOSTNAME + ":" + mongoDevService.getConfig().get("port") + "mongodb://" + mongoDevService.getConfig().get("host") + ":" + mongoDevService.getConfig().get("port") ); - + output.add(mongoDevService.toBuildItem()); } if (config.devservices().kafka().enabled()) {//connect to existent @@ -155,21 +174,23 @@ public List createContainer(LaunchModeBuildItem laun } {//Core API DevServicesResultBuildItem.RunningDevService coreApiDevService = DEVSERVICES.get("coreApi"); - + if (coreApiDevService == null) { OqmCoreApiWebServiceContainer container = this.newCoreApiContainer(config, mongoConnectionInfo, kafkaConnectionInfo); - + Map props = new HashMap<>(); props.put(Constants.CONFIG_ROOT_NAME + ".baseUri", "http://" + container.getHost() + ":" + container.getPort()); props.put("quarkus.rest-client.\"" + Constants.CORE_API_CLIENT_NAME + "\".url", "${" + Constants.CONFIG_ROOT_NAME + ".baseUri}"); - + + log.info("Core API devservice properties: " + props); + coreApiDevService = new DevServicesResultBuildItem.RunningDevService(FEATURE, container.getContainerId(), container::close, props); DEVSERVICES.put("coreApi", coreApiDevService); } - + output.add(coreApiDevService.toBuildItem()); } - + if (firstSetup) { firstSetup = false; closeBuildItem.addCloseTask( @@ -184,28 +205,28 @@ public List createContainer(LaunchModeBuildItem laun log.error("Failed to close devservice: " + curDevservice.toString(), e); } } - + firstSetup = true; }, true ); } - + return output; } - + @BuildStep(onlyIf = IsLocalDevelopment.class) void setupDevUiCard(BuildProducer cardsProducer, CoreApiLibBuildTimeConfig config) { - + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); cardPageBuildItem.setLogo("oqm-icon.svg", "oqm-icon.svg"); - + //show oqm core api ui cardPageBuildItem.addPage( Page.externalPageBuilder("OQM Core API UI") .url("http://localhost:" + config.devservices().port()) .doNotEmbed()//needed as embedded fails due to CORS ); - + //page for managing core api data cardPageBuildItem.addPage( Page.webComponentPageBuilder() @@ -213,10 +234,10 @@ void setupDevUiCard(BuildProducer cardsProducer, CoreApiLibBu .icon("font-awesome-solid:database") .componentLink("qwc-oqm-core-api-lib-db-management.js") ); - + cardsProducer.produce(cardPageBuildItem); } - + @BuildStep(onlyIf = IsLocalDevelopment.class) JsonRPCProvidersBuildItem createJsonRPCService() { return new JsonRPCProvidersBuildItem(tech.ebp.oqm.lib.core.api.quarkus.runtime.dev.CoreApiDevDbManagementService.class); diff --git a/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/config/CoreApiLibBuildTimeConfig.java b/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/config/CoreApiLibBuildTimeConfig.java index 13b06c87aa..3d46e28f04 100644 --- a/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/config/CoreApiLibBuildTimeConfig.java +++ b/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/config/CoreApiLibBuildTimeConfig.java @@ -96,7 +96,7 @@ interface ImageConfig { * * @return The version/ tag of the core api container image */ - @WithDefault("5.0.0") + @WithDefault("6.0.0-SNAPSHOT") String version(); /** diff --git a/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/testContainers/OqmCoreApiWebServiceContainer.java b/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/testContainers/OqmCoreApiWebServiceContainer.java index 95ccb08fa6..7c285674bc 100644 --- a/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/testContainers/OqmCoreApiWebServiceContainer.java +++ b/software/libs/core-api-lib-quarkus/deployment/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/deployment/testContainers/OqmCoreApiWebServiceContainer.java @@ -1,6 +1,7 @@ package tech.ebp.oqm.lib.core.api.quarkus.deployment.testContainers; import com.github.dockerjava.api.model.HostConfig; +import io.quarkus.devservices.common.ConfigureUtil; import org.testcontainers.Testcontainers; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; @@ -13,11 +14,11 @@ * Container for the Open QuarterMaster Core API web service. */ public class OqmCoreApiWebServiceContainer extends GenericContainer { - + private final CoreApiLibBuildTimeConfig.DevserviceConfig devserviceConfig; private final Map mongoConnectionInfo; private final Map kafkaConnectionInfo; - + /** * Initializes the container * @param devserviceConfig The devservice config to use to set this container up. @@ -32,27 +33,31 @@ public OqmCoreApiWebServiceContainer( this.mongoConnectionInfo = mongoConnectionInfo; this.kafkaConnectionInfo = kafkaConnectionInfo; } - + @Override protected void configure() { //configure network + + //TODO:: #1287 these lines are related to not host netowrking +// ConfigureUtil.configureSharedNetwork(this, "oqm-core-api"); +// this.addFixedExposedPort(devserviceConfig.port(), 80); +// this.withAccessToHost(true); + + //TODO:: #1287 don't do this withCreateContainerCmdModifier(cmd -> cmd.withHostConfig( new HostConfig().withNetworkMode("host") )); -// Testcontainers.exposeHostPorts(this.devserviceConfig.keycloak().port()); -// Testcontainers.exposeHostPorts(this.devserviceConfig.kafka().port()); - -// addFixedExposedPort(devserviceConfig.port(), 80); - + this.withEnv("QUARKUS_HTTP_PORT", String.valueOf(this.devserviceConfig.port())); + + //configure env this.withEnv(mongoConnectionInfo); this.withEnv(kafkaConnectionInfo); - this.withEnv("QUARKUS_HTTP_PORT", String.valueOf(this.devserviceConfig.port())); - + // Tell the dev service how to know the container is ready. All 3 is likely overkill, but eh this.waitingFor(Wait.forHealthcheck()); this.waitingFor(Wait.forLogMessage(".*oqm-core-api .* started.*", 1)); - + //don't need to do this, the docker healthcheck covers this. Not included as logs a superfluous stacktrace at startup. // this.waitingFor(Wait.forHttp("/q/health").forResponsePredicate((String response)->{ // ObjectNode status; @@ -64,7 +69,7 @@ protected void configure() { // return status.get("status").asText().equals("UP"); // })); } - + /** * Gets the port that the dev service is listening on. * @return the port that the dev service is listening on. diff --git a/software/libs/core-api-lib-quarkus/deployment/src/test/java/tech/ebp/oqm/lib/core/api/quark/quarkus/test/CoreApiLibHealthCheckTest.java b/software/libs/core-api-lib-quarkus/deployment/src/test/java/tech/ebp/oqm/lib/core/api/quark/quarkus/test/CoreApiLibHealthCheckTest.java index d2a0de231d..a4fa0dbfcd 100644 --- a/software/libs/core-api-lib-quarkus/deployment/src/test/java/tech/ebp/oqm/lib/core/api/quark/quarkus/test/CoreApiLibHealthCheckTest.java +++ b/software/libs/core-api-lib-quarkus/deployment/src/test/java/tech/ebp/oqm/lib/core/api/quark/quarkus/test/CoreApiLibHealthCheckTest.java @@ -9,7 +9,7 @@ import tech.ebp.oqm.lib.core.api.quarkus.runtime.Constants; public class CoreApiLibHealthCheckTest { - + @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withEmptyApplication() @@ -19,14 +19,16 @@ public class CoreApiLibHealthCheckTest { // .withConfigurationResource("application-default-datasource.properties") // .overrideConfigKey("quarkus.datasource.health.enabled", "true") ; - + @Test - public void testDataSourceHealthCheckExclusion() { + public void testDataSourceHealthCheckExclusion() throws InterruptedException { ValidatableResponse response = RestAssured.when().get("/q/health") .then(); - + System.out.println(response.extract().body().asPrettyString()); - + +// Thread.sleep(60_000); + response.statusCode(200) .body("status", CoreMatchers.equalTo("UP")); } diff --git a/software/libs/core-api-lib-quarkus/integration-tests/pom.xml b/software/libs/core-api-lib-quarkus/integration-tests/pom.xml index 6642e108e6..0015530cf1 100644 --- a/software/libs/core-api-lib-quarkus/integration-tests/pom.xml +++ b/software/libs/core-api-lib-quarkus/integration-tests/pom.xml @@ -5,7 +5,7 @@ tech.ebp.oqm.lib.core.api.quark core-api-lib-quarkus-parent - 5.0.0 + 6.0.0-SNAPSHOT core-api-lib-quarkus-integration-tests Core Api Lib Quarkus - Integration Tests diff --git a/software/libs/core-api-lib-quarkus/pom.xml b/software/libs/core-api-lib-quarkus/pom.xml index 52af7064d9..5532f2800e 100644 --- a/software/libs/core-api-lib-quarkus/pom.xml +++ b/software/libs/core-api-lib-quarkus/pom.xml @@ -7,7 +7,7 @@ 4.0.0 tech.epic-breakfast-productions.openquartermaster.lib.core core-api-lib-quarkus-parent - 5.0.0 + 6.0.0-SNAPSHOT pom Core Api Lib Quarkus - Parent The parent pom package for the Core API Library for Quarkus. diff --git a/software/libs/core-api-lib-quarkus/runtime/pom.xml b/software/libs/core-api-lib-quarkus/runtime/pom.xml index 5569fc592b..fa92e460ae 100644 --- a/software/libs/core-api-lib-quarkus/runtime/pom.xml +++ b/software/libs/core-api-lib-quarkus/runtime/pom.xml @@ -8,7 +8,7 @@ tech.epic-breakfast-productions.openquartermaster.lib.core core-api-lib-quarkus-parent - 5.0.0 + 6.0.0-SNAPSHOT core-api-lib-quarkus Core Api Lib Quarkus - Runtime diff --git a/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/dev/CoreApiDevDbManagementService.java b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/dev/CoreApiDevDbManagementService.java index d8d5b3b078..9aef04eb3e 100644 --- a/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/dev/CoreApiDevDbManagementService.java +++ b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/dev/CoreApiDevDbManagementService.java @@ -19,34 +19,34 @@ @IfBuildProfile(anyOf = {"dev", "test"}) @ApplicationScoped public class CoreApiDevDbManagementService { - + @RestClient OqmCoreApiClientService oqmCoreApiClient; - + @Inject KcClientAuthService serviceAccountService; - + @Inject ObjectMapper objectMapper; - - + + private int fileNum = 0; private int blockNum = 0; private int itemNum = 0; - + private FileUploadBody newFile(String type) { FileUploadBody.Builder builder = FileUploadBody.builder(); - + builder.description("File " + (++fileNum) + " (" + type + ")"); builder.source("populator"); - + switch (type) { case "image": builder.fileName("image.jpg"); builder.file(new ByteArrayInputStream( Base64.getDecoder().decode( "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCADqATkDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECA//EACQQAQEBAAIBBAMBAQEBAAAAAAABESExQQISUXFhgZGxocHw/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/AMriLyCKgg1gQwCgs4FTMOdutepjQak+FzMSVqgxZdRdPPIIvH5WzzGdBriphtTeAXg2ZjKA1pqKDUGZca3foBek8gFv8Ie3fKdA1qb8s7hoL6eLVt51FsAnql3Ut1M7AWbflLMDkEMX/F6/YjK/pADFQAUNA6alYagKk72m/j9p4Bq2fDDSYKLNXPNLoHE/NT6RYC31cJxZ3yWVM+aBYi/S2ZgiAsnYJx5D21vPmqrm3PTfpQQwyAC8JZvSKDni41ZrMuUVVl+Uz9w9v/1QWrZsZ5nFPHYH+JZyureQSF5M+fJ0CAfwRAVRBQA1DAWVUayoJUWoDpsxntPsueBV4+VxhdyAtv8AjOLGpIDMLbeGvbF4iozJfr/WukAVABAXAQXEAAASzVAZdO2WNordm+emFl7XcQSNZiFtv0C9w90nhJf4mA1u+GcJFwIyAqL/AOovwgGNfSRqdIrNa29M0gKCAojU9PAMjWXpckEJFNFEAAXEUBABYz6rZ0ureQc9vyt9XxDF2QAXtABcQAs0AZywkvluJbyipifas52DcyxjlZweAO0xri/hc+wZOEKIu6nSyeToVZyWXwvCg53gW81QQ7aTNAn5dGZJPs1UXURQAUEMCXQLZE93PRZ5hPTgNMrbIzKCm52LZwCs+2M8w2g3sjPuZAXb4IsMAUACzVUGM4/K+md6vEXUUyM5PDR0IxYe6ramih0VNBrS4xoqN8Q1BFQk3yqyAsioioAAKgDSJL4/jQIn5igLrPqtOuf6oOaxbMoAltUAhhIoJiiggrPu+AaOIxtAX3JbaAIaLwi4t9X4T3fg2AFtqcrUUarP20zUDAmqoE0WRBZPNVUVEAAAAVAC8kvih2DSKxOdBqs7Z0l0gI0mKAC4AuHE7ZtBriM+744QAAAAABAFsveIttBICyaikvy1+r/Cen5rWQHIBQa4rIDRqSl5qDWqziqgAAAATA7BpGdqXb2C2+J/UgAtRQBSQtkBWb6vhLbQAAAAAEBRAAAAAUbm+GZNdPxAP+ql2Tjwx7/wIgZ8iKvBk+CJoCXii9gaqZ/qqihAAAEVABGkBFUwBftNkZ3QW34QAAABFAQAVAAAAAARVkl8gs/43sk1jL45LvHArepk+E9XTG35oLqsmIKmLAEygKg0y1AFQBUXwgAAAoBC34S3UAAABAVAAAAAABAUQAVABdRQa1PcYyit2z58M8C4ouM2NXpOEGeWtNZUatiAIoAKIoCoAoG4C9MW6dgIoAIAAAAAAACKWAgL0CAAAALiANCKioNLgM1CrLihmTafkt1EF3SZ5ZVUW4mnIKvAi5fhEURVDWVQBRAAAAAAAAQFRVyAyulgAqCKlF8IqLsEgC9mGoC+IusqCrv5ZEUVOk1RuJfwSLOOkGFi4XPCoYYrNiKauosBGi9ICstM1UAAAAAAFQ0VcTBAXUGgIqGoKhKAzRRUQUAwxoSrGRpkQA/qiosOL9oJptMRRVZa0VUqSiChE6BqMgCwqKqIogAIAqKCKgKoogg0lBFuIKgAAAKNRlf2gqsftsEtZWoAAqAACKoMqAAeSoqp39kL2AqLOlE8rEBFQARYALhigrNC9gGmooLp4TweEQFFBFAECgIoAu0ifIAqAAA//9k=" - ) + ) )); break; case "file": @@ -54,75 +54,73 @@ private FileUploadBody newFile(String type) { builder.file(new ByteArrayInputStream("This is a test file.".getBytes())); break; } - + return builder.build(); } - + private ObjectNode newStorageBlock() { return objectMapper.createObjectNode() .put("label", "Storage Block " + (++blockNum)); } - + private ObjectNode newItem(String storageType) { return objectMapper.createObjectNode() .put("storageType", storageType) .put("name", "Inventory Item " + (++itemNum)); } - + private ObjectNode newStored(String storedType, ObjectNode item) { ObjectNode output = objectMapper.createObjectNode() - .put("type", storedType) - .set("item", item.get("id")) - ; - - switch (storedType){ + .put("type", storedType) + .set("item", item.get("id")); + + switch (storedType) { case "AMOUNT": ObjectNode amt = output.putObject("amount"); amt.put("value", 0); amt.set("unit", item.get("unit")); amt.put("scale", "ABSOLUTE"); - + break; case "UNIQUE": - + break; } - + return output; } - - + + public String resetDb() { log.info("Resetting all OQM DB's."); - + this.oqmCoreApiClient.manageDbClearAll(this.serviceAccountService.getAuthString()).await().indefinitely(); - + this.fileNum = 0; this.blockNum = 0; this.itemNum = 0; - + log.info("DONE resetting all OQM DB's."); - + return "OK"; } - + public String resetAndPopulateDb(String db) { String resetResult = this.resetDb(); - + log.info("Populating OQM DB: {}", db); //TODO:: populate from files? - + ObjectNode image1 = this.oqmCoreApiClient.imageAdd(this.serviceAccountService.getAuthString(), db, newFile("image")).await().indefinitely(); ObjectNode image2 = this.oqmCoreApiClient.imageAdd(this.serviceAccountService.getAuthString(), db, newFile("image")).await().indefinitely(); - + ObjectNode file1 = this.oqmCoreApiClient.fileAttachmentAdd(this.serviceAccountService.getAuthString(), db, newFile("file")).await().indefinitely(); - + ArrayNode images = objectMapper.createArrayNode(); images.add(image1.get("id")); images.add(image2.get("id")); - - - + + ObjectNode storageBlock1 = this.oqmCoreApiClient.storageBlockAdd(this.serviceAccountService.getAuthString(), db, newStorageBlock().set("imageIds", images)).await().indefinitely(); ObjectNode storageBlock2 = this.oqmCoreApiClient.storageBlockAdd(this.serviceAccountService.getAuthString(), db, newStorageBlock()).await().indefinitely(); @@ -133,111 +131,126 @@ public String resetAndPopulateDb(String db) { this.serviceAccountService.getAuthString(), db, newStorageBlock().set("parent", storageBlock4.get("id")) ).await().indefinitely(); - + //TODO:: categories - + //TODO:: id generators - - + + {// item - bulk - ArrayNode blocks = objectMapper.createArrayNode() - .add(storageBlock1.get("id")) - .add(storageBlock2.get("id")); + log.info("Creating bulk item: {}", this.newItem("BULK") + .set( + "storageBlocks", + objectMapper.createArrayNode() + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock1.get("id"))) + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock2.get("id"))) + )); ObjectNode item = this.oqmCoreApiClient.invItemCreate( this.serviceAccountService.getAuthString(), db, this.newItem("BULK") - .set("storageBlocks", blocks) + .set( + "storageBlocks", + objectMapper.createArrayNode() + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock1.get("id"))) + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock2.get("id"))) + ) ).await().indefinitely(); - + ObjectNode newStored = this.newStored("AMOUNT", item); - + ObjectNode curTransaction = objectMapper.createObjectNode().put("type", "ADD_AMOUNT"); curTransaction.set("toBlock", storageBlock1.get("id")); - + ObjectNode amt = curTransaction.putObject("amount"); amt.put("value", 5); amt.set("unit", item.get("unit")); amt.put("scale", "ABSOLUTE"); - + this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); } {// item - Amt list - ArrayNode blocks = objectMapper.createArrayNode() - .add(storageBlock1.get("id")) - .add(storageBlock2.get("id")); ObjectNode item = this.oqmCoreApiClient.invItemCreate( this.serviceAccountService.getAuthString(), db, this.newItem("AMOUNT_LIST") - .set("storageBlocks", blocks) + .set( + "storageBlocks", + objectMapper.createArrayNode() + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock1.get("id"))) + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock2.get("id"))) + ) ).await().indefinitely(); - + ObjectNode newStored = this.newStored("AMOUNT", item); - - ((ObjectNode)newStored.get("amount")).put("value", 5); - + + ((ObjectNode) newStored.get("amount")).put("value", 5); + ObjectNode curTransaction = objectMapper.createObjectNode().put("type", "ADD_WHOLE"); curTransaction.set("toBlock", storageBlock1.get("id")); curTransaction.set("toAdd", newStored); - + this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); - - ((ObjectNode)newStored.get("amount")).put("value", 4); + + ((ObjectNode) newStored.get("amount")).put("value", 4); this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); - - ((ObjectNode)newStored.get("amount")).put("value", 3); + + ((ObjectNode) newStored.get("amount")).put("value", 3); this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); - - ((ObjectNode)newStored.get("amount")).put("value", 2); + + ((ObjectNode) newStored.get("amount")).put("value", 2); this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); } {// item - Unique Multi - ArrayNode blocks = objectMapper.createArrayNode() - .add(storageBlock1.get("id")) - .add(storageBlock2.get("id")); ObjectNode item = this.oqmCoreApiClient.invItemCreate( this.serviceAccountService.getAuthString(), db, this.newItem("UNIQUE_MULTI") - .set("storageBlocks", blocks) + .set( + "storageBlocks", + objectMapper.createArrayNode() + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock1.get("id"))) + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock2.get("id"))) + ) ).await().indefinitely(); - + ObjectNode newStored = this.newStored("UNIQUE", item); - + ObjectNode curTransaction = objectMapper.createObjectNode().put("type", "ADD_WHOLE"); curTransaction.set("toBlock", storageBlock1.get("id")); curTransaction.set("toAdd", newStored); - + this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); } {// item - Unique Single - ArrayNode blocks = objectMapper.createArrayNode() - .add(storageBlock1.get("id")) - .add(storageBlock2.get("id")); ObjectNode item = this.oqmCoreApiClient.invItemCreate( this.serviceAccountService.getAuthString(), db, this.newItem("UNIQUE_SINGLE") - .set("storageBlocks", blocks) + .set( + "storageBlocks", + objectMapper.createArrayNode() + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock1.get("id"))) + .add(objectMapper.createObjectNode().set("storageBlock", storageBlock2.get("id"))) + ) ).await().indefinitely(); - + ObjectNode newStored = this.newStored("UNIQUE", item); - + ObjectNode curTransaction = objectMapper.createObjectNode().put("type", "ADD_WHOLE"); curTransaction.set("toBlock", storageBlock1.get("id")); curTransaction.set("toAdd", newStored); - + this.oqmCoreApiClient.invItemStoredTransact(this.serviceAccountService.getAuthString(), db, item.get("id").asText(), curTransaction).await().indefinitely(); } - + //TODO:: checkouts - + log.info("DONE populating OQM DB: {}", db); return "OK"; } - + } diff --git a/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/messaging/EventNotificationWrapper.java b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/messaging/EventNotificationWrapper.java new file mode 100644 index 0000000000..35526c0a99 --- /dev/null +++ b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/messaging/EventNotificationWrapper.java @@ -0,0 +1,28 @@ +package tech.ebp.oqm.lib.core.api.quarkus.runtime.messaging; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * A convenience object to use when reading history event notifications. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Setter(AccessLevel.PROTECTED) +public class EventNotificationWrapper { + /** The database the event occurred in. */ + private String database; + /** The name of the object that was affected. */ + private String objectName; + /** The type of event that occurred. */ + private String eventType; + /** The id of the object that was affected. */ + private String objectId; + /** The event data. */ + private ObjectNode event; +} diff --git a/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/messaging/HistoryEventFilter.java b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/messaging/HistoryEventFilter.java new file mode 100644 index 0000000000..5b8e676302 --- /dev/null +++ b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/messaging/HistoryEventFilter.java @@ -0,0 +1,74 @@ +package tech.ebp.oqm.lib.core.api.quarkus.runtime.messaging; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.Collection; + +/** + * Filter utilities for history events. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class HistoryEventFilter { + + /** + * A filter method accepting a history event and filter options. + * @param event The event to filter. + * @param filterOptions The options to filter by. + * @return If the event passes the filter. + */ + public static boolean filter( + EventNotificationWrapper event, + @NonNull + FilterOptions filterOptions + ){ + if(filterOptions.getDatabaseId() != null){ + if(!filterOptions.getDatabaseId().equals(event.getDatabase())){ + return false; + } + } + + if(filterOptions.getObjectIds() != null && !filterOptions.getObjectIds().isEmpty()){ + if(!filterOptions.getObjectIds().contains(event.getObjectId())){ + return false; + } + } + + if(filterOptions.getObjectName() != null && !filterOptions.getObjectName().isEmpty()){ + if(!filterOptions.getObjectName().contains(event.getObjectName())){ + return false; + } + } + + if(filterOptions.getEventType() != null && !filterOptions.getEventType().isEmpty()){ + if(!filterOptions.getEventType().contains(event.getEventType())){ + return false; + } + } + + return true; + } + + /** + * Filter options for the history event filter. + *

    + * Fields are optional and are AND'ed together in the filter. + *

    + * Values in lists are OR'ed together. + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class FilterOptions { + private String databaseId; + + private Collection objectIds; + private Collection objectName; + private Collection eventType; + } +} diff --git a/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/restClient/models/InteractingEntity.java b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/restClient/models/InteractingEntity.java index deacc8d5e2..2fe3712aaf 100644 --- a/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/restClient/models/InteractingEntity.java +++ b/software/libs/core-api-lib-quarkus/runtime/src/main/java/tech/ebp/oqm/lib/core/api/quarkus/runtime/restClient/models/InteractingEntity.java @@ -16,9 +16,15 @@ public class InteractingEntity { private String idFromAuthProvider; private String authProvider; private String name; + private String username; private String email; private String type; private Set roles; private List keywords; private Map attributes; + + public String getInitials() { + String[] nameParts = this.name.split(" ", 2); + return "" + nameParts[0].charAt(0) + nameParts[1].charAt(0); + } }