Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98e3771c27
|
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased / 0.2.0
|
||||
|
||||
- Add an optional unique catalog slug and allow absent catalog prices without changing required deal-line prices. Ship the additive `crm-product-catalog.changelog.sql`; hosts must include it before adopting the metadata.
|
||||
- Add opt-in `productPermissionBase` configuration for native catalog-only read/write permissions. Existing hosts retain `crm.manage` behavior; opting into `crm.product` requires the role migration documented in UPGRADE.md.
|
||||
|
||||
- Read the CRM QBit metadata version from the Maven-filtered artifact version so release metadata matches the build.
|
||||
|
||||
- Adopt QQQ 4.1.0-RC.7 native help bundles, replacing the RC.6 stand-in loader with host-neutral CRM help, live references and the core coverage gate.
|
||||
|
||||
@@ -56,6 +56,7 @@ qInstance.setEnforcePermissionsInCoreActions(true);
|
||||
|
||||
- `defaultBackendNameForTables`: required backend name.
|
||||
- `inferBackendNames`: defaults to true; sets snake_case SQL column names while Java/QQQ field names remain camelCase. SQL table names equal the QQQ table names. Set false only for an explicit host column mapping.
|
||||
- `withProductPermissionBase("crm.product")`: opt into catalog-only `crm.product.read/write` permissions. The default remains `crm.manage` for existing hosts. Before opting in, preserve existing catalog access by adding the corresponding product grants to roles that already hold `crm.manage.read/write`; Marketing catalog readers receive only `crm.product.read`. This does not grant CRM app/settings access or product writes to Marketing. See UPGRADE.md for the required schema and role migration.
|
||||
- `tableMetaDataCustomizer`: optional host table customizer.
|
||||
- `withTenantSecurityKeyType("tenant")`: enables nullable `tenantKey` fields and record locks. It defaults to null (off). `withTenantAllAccessKeyName("allTenants")` names the host's all-access key. Existing host key types are preserved.
|
||||
- Tenant-enabled hosts separately include `src/main/resources/db/liquibase/qbit-crm/crm-tenancy.changelog.sql` **after** the base changelog and explicitly select the `crm-tenancy` context. Single-tenant hosts omit that file. Child records use parent join locks; polymorphic external mappings and internal job checkpoints carry direct tenant keys. Hosts must supply tenant keys on new direct-key records.
|
||||
|
||||
+17
@@ -1,5 +1,22 @@
|
||||
# Upgrading
|
||||
|
||||
## Shared product catalog (unreleased)
|
||||
|
||||
`crm_product` now supports an optional unique `slug` (64 characters) and an absent catalog `unit_price` for informational products. No price is invented. Deal-line `unit_price` remains required; existing prices and amounts are unchanged.
|
||||
|
||||
Before running the updated metadata against an existing schema, include `db/liquibase/qbit-crm/crm-product-catalog.changelog.sql` after the base CRM schema. This additive migration also works for hosts that copied the old CRM DDL; do not replay the base changelog against an already-created schema. Fresh installs using `crm.changelog.sql` must include the catalog changelog too. `docs/ddl/postgres.sql` describes the final schema, so hosts creating tables directly from that file must not then replay the additive migration.
|
||||
|
||||
The migration adds nullable `slug`, its unique constraint, and drops only the catalog price NOT NULL constraint. It preserves product IDs, timestamps, prices and tenant keys. It does not migrate Marketing data or alter any deal-line price. Roll back application code only after checking schema compatibility; do not drop populated slugs or force null prices to zero.
|
||||
|
||||
Catalog permissions stay on `crm.manage.read/write` unless the host opts into `withProductPermissionBase("crm.product")`. Opt-in requires an explicit role migration in the same adoption rollout:
|
||||
|
||||
- Existing `crm.manage.read` holders retain that grant and gain `crm.product.read`.
|
||||
- Existing `crm.manage.write` holders retain that grant and gain `crm.product.write` (preserve their existing read grant separately).
|
||||
- Marketing roles needing catalog selection receive `crm.product.read` only. Their marketing permissions remain unchanged.
|
||||
- Do not grant `crm.manage` to Marketing users or infer permissions from navigation placement.
|
||||
|
||||
The native QQQ permission checks enforce both API writes and UI operation checks. Tenant locks continue to apply. Marketing integration and host data migration must be adopted separately before retiring `marketing_product`.
|
||||
|
||||
## Starter defaults (Unreleased / 0.2.0)
|
||||
|
||||
The CRM now ships `db/liquibase/qbit-crm/crm-defaults.changelog.sql`, a separate insert-only changelog. Without it,
|
||||
|
||||
@@ -565,13 +565,15 @@ CREATE TABLE IF NOT EXISTS crm_product (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
slug VARCHAR(64),
|
||||
description TEXT,
|
||||
unit_price DECIMAL(20,6) NOT NULL,
|
||||
unit_price DECIMAL(20,6),
|
||||
currency_code VARCHAR(3) DEFAULT 'USD',
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_product_1 UNIQUE (sku)
|
||||
CONSTRAINT uq_crm_product_1 UNIQUE (sku),
|
||||
CONSTRAINT uq_crm_product_slug UNIQUE (slug)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_sales_goal (
|
||||
|
||||
@@ -41,6 +41,7 @@ public class CrmQBitConfig implements QBitConfig
|
||||
{
|
||||
public static final CrmQBitConfig DEFAULT_CONFIG = new CrmQBitConfig();
|
||||
|
||||
private String productPermissionBase = "crm.manage";
|
||||
private String tenantSecurityKeyType;
|
||||
private String tenantAllAccessKeyName;
|
||||
private String apiName;
|
||||
@@ -69,12 +70,45 @@ public class CrmQBitConfig implements QBitConfig
|
||||
@Override
|
||||
public void validate(com.kingsrook.qqq.backend.core.model.metadata.QInstance qInstance, List<String> errors)
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(productPermissionBase), "CrmQBitConfig.productPermissionBase is required", errors);
|
||||
assertCondition(StringUtils.hasContent(defaultBackendNameForTables),
|
||||
"CrmQBitConfig.defaultBackendNameForTables is required", errors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Catalog permission base. Defaults to crm.manage for existing hosts.
|
||||
** Shared-catalog hosts may use crm.product after migrating role grants.
|
||||
*******************************************************************************/
|
||||
public String getProductPermissionBase()
|
||||
{
|
||||
return (productPermissionBase);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the catalog's native read/write permission base.
|
||||
*******************************************************************************/
|
||||
public void setProductPermissionBase(String productPermissionBase)
|
||||
{
|
||||
this.productPermissionBase = productPermissionBase;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the catalog's native read/write permission base fluently.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withProductPermissionBase(String productPermissionBase)
|
||||
{
|
||||
setProductPermissionBase(productPermissionBase);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultBackendNameForTables
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -208,6 +208,10 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
addOrComposeCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(CrmRelationshipValidator.class));
|
||||
}
|
||||
table.setPermissionRules(CrmPermissions.tableRules(tableName));
|
||||
if("crm_product".equals(tableName))
|
||||
{
|
||||
table.getPermissionRules().setPermissionBaseName(qBitConfig.getProductPermissionBase());
|
||||
}
|
||||
table.setSourceQBitName(getQBitMetaData().getName());
|
||||
if(table.getSections() != null)
|
||||
{
|
||||
|
||||
@@ -87,7 +87,8 @@ public class Product extends QRecordEntity
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withUniqueKey(new UniqueKey("sku"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "sku", "isActive"))
|
||||
.withUniqueKey(new UniqueKey("slug"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "slug", "sku", "isActive"))
|
||||
.withSection(new QFieldSection("pricing", new QIcon("payments"), Tier.T2,
|
||||
List.of("unitPrice", "currencyCode")))
|
||||
.withSection(SectionFactory.defaultT2("description"))
|
||||
@@ -145,10 +146,13 @@ public class Product extends QRecordEntity
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String sku;
|
||||
|
||||
@QField(maxLength = 64, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String slug;
|
||||
|
||||
@QField()
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true)
|
||||
@QField()
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@QField(maxLength = 3, valueTooLongBehavior = ValueTooLongBehavior.ERROR, defaultValue = "USD")
|
||||
@@ -184,6 +188,37 @@ public class Product extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional stable catalog slug for cross-module references.
|
||||
*******************************************************************************/
|
||||
public String getSlug()
|
||||
{
|
||||
return (slug);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the catalog slug.
|
||||
*******************************************************************************/
|
||||
public void setSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the catalog slug fluently.
|
||||
*******************************************************************************/
|
||||
public Product withSlug(String slug)
|
||||
{
|
||||
setSlug(slug);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
--liquibase formatted sql
|
||||
-- Run after the existing CRM base schema, before deploying shared catalog metadata.
|
||||
-- Existing product identities, prices, deal-line amounts and tenant keys are preserved.
|
||||
--changeset qbit-crm:crm_product_shared_catalog
|
||||
ALTER TABLE crm_product ADD COLUMN slug VARCHAR(64);
|
||||
ALTER TABLE crm_product ADD CONSTRAINT uq_crm_product_slug UNIQUE (slug);
|
||||
ALTER TABLE crm_product ALTER COLUMN unit_price DROP NOT NULL;
|
||||
@@ -521,7 +521,7 @@ entries:
|
||||
content: |
|
||||
Use **{{label:crm_product}}** to review products and services available as deal line items.
|
||||
|
||||
**How rows get created:** Add your products or services with **New**, including their prices and currencies. You can then select them on deal product lines.
|
||||
**How rows get created:** Add your products or services with **New**, with a price and currency when known. Informational products may leave the catalog price blank; a deal line still requires its own agreed unit price.
|
||||
- key: table:crm_product;section:identity
|
||||
summary: "The identifying details for this record."
|
||||
- key: table:crm_product;section:pricing
|
||||
@@ -545,7 +545,9 @@ entries:
|
||||
- key: table:crm_product;field:modifyDate
|
||||
summary: "When this record was last changed."
|
||||
- key: table:crm_product;field:unitPrice
|
||||
summary: "The price for one unit of the product."
|
||||
summary: "Optional catalog price per unit. Leave blank when not priced; deal lines require an agreed price."
|
||||
- key: table:crm_product;field:slug
|
||||
summary: "Optional unique stable identifier for this shared catalog product, up to 64 characters."
|
||||
- key: table:crm_product;field:sku
|
||||
summary: "The product code used to identify the item."
|
||||
- key: table:crm_deal_stage_history;slot:help
|
||||
|
||||
@@ -37,6 +37,52 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*******************************************************************************/
|
||||
class CrmPermissionsTest extends BaseTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Legacy hosts retain management access; the shared catalog is opt-in.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDefaultCatalogPermissions()
|
||||
{
|
||||
QueryInput input = new QueryInput("crm_product");
|
||||
QContext.getQSession().setPermissions(Set.of(CrmPermissions.MANAGE_READ, CrmPermissions.MANAGE_WRITE));
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_product", TablePermissionSubType.READ)).isTrue();
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_product", TablePermissionSubType.EDIT)).isTrue();
|
||||
QContext.getQSession().setPermissions(Set.of("crm.product.read"));
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_product", TablePermissionSubType.READ)).isFalse();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Native catalog read grants cannot write or open unrelated CRM settings.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSharedCatalogPermissions() throws Exception
|
||||
{
|
||||
com.kingsrook.qqq.backend.core.model.metadata.QInstance instance = new com.kingsrook.qqq.backend.core.model.metadata.QInstance();
|
||||
instance.withInstanceDefaultAuthentication(new com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData()
|
||||
.withName("anonymous").withType(com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType.FULLY_ANONYMOUS));
|
||||
instance.addBackend(QContext.getQInstance().getBackend(BACKEND_NAME));
|
||||
new CrmQBitProducer().withQBitConfig(new CrmQBitConfig().withDefaultBackendNameForTables(BACKEND_NAME)
|
||||
.withProductPermissionBase("crm.product")).produce(instance).addSelfToInstance(instance);
|
||||
QContext.init(instance, QContext.getQSession());
|
||||
QueryInput input = new QueryInput("crm_product");
|
||||
QContext.getQSession().setPermissions(Set.of("crm.product.read"));
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_product", TablePermissionSubType.READ)).isTrue();
|
||||
for(TablePermissionSubType write : Set.of(TablePermissionSubType.INSERT, TablePermissionSubType.EDIT, TablePermissionSubType.DELETE))
|
||||
{
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_product", write)).isFalse();
|
||||
}
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_pipeline", TablePermissionSubType.READ)).isFalse();
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_contact", TablePermissionSubType.READ)).isFalse();
|
||||
assertThat(PermissionsHelper.hasProcessPermission(input, "crm_merge_contacts")).isFalse();
|
||||
QContext.getQSession().setPermissions(Set.of(CrmPermissions.MANAGE_READ, CrmPermissions.MANAGE_WRITE, "crm.product.read", "crm.product.write"));
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_product", TablePermissionSubType.EDIT)).isTrue();
|
||||
assertThat(PermissionsHelper.hasTablePermission(input, "crm_pipeline", TablePermissionSubType.EDIT)).isTrue();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A read grant does not allow writes or settings access.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -41,6 +41,22 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class CrmQBitConfigTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Catalog permission bases must be explicit; legacy behavior is the default.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCatalogPermissionBase()
|
||||
{
|
||||
CrmQBitConfig config = new CrmQBitConfig().withDefaultBackendNameForTables("memory");
|
||||
assertThat(config.getProductPermissionBase()).isEqualTo("crm.manage");
|
||||
config.setProductPermissionBase(" ");
|
||||
List<String> errors = new ArrayList<>();
|
||||
config.validate(new QInstance(), errors);
|
||||
assertThat(errors).contains("CrmQBitConfig.productPermissionBase is required");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test all fluent setters and corresponding getters.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -281,6 +281,7 @@ class CrmRdbmsTest
|
||||
var freshColumns = schemaColumns();
|
||||
executeSql("DROP ALL OBJECTS;");
|
||||
executeSql(changelog);
|
||||
executeSql(Files.readString(Path.of("src/main/resources/db/liquibase/qbit-crm/crm-product-catalog.changelog.sql")));
|
||||
assertThat(schemaColumns()).isEqualTo(freshColumns);
|
||||
insert("crm_contact", new QRecord().withValue("email", "email-only@example.test"));
|
||||
}
|
||||
|
||||
@@ -52,6 +52,25 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
class DealProductTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional catalog pricing does not make the agreed line price optional.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testUnpricedCatalogStillRequiresLinePrice() throws QException
|
||||
{
|
||||
Integer productId = insertProduct("Unpriced service", "UNPRICED", null);
|
||||
InsertOutput missing = new InsertAction().execute(new InsertInput(DealProduct.TABLE_NAME)
|
||||
.withRecordEntity(new DealProduct().withDealId(1).withProductId(productId).withQuantity(BigDecimal.ONE)));
|
||||
assertThat(missing.getRecords().get(0).getErrors()).isNotEmpty();
|
||||
InsertOutput agreed = new InsertAction().execute(new InsertInput(DealProduct.TABLE_NAME)
|
||||
.withRecordEntity(new DealProduct().withDealId(1).withProductId(productId).withQuantity(new BigDecimal("2"))
|
||||
.withUnitPrice(new BigDecimal("12.50"))));
|
||||
assertThat(agreed.getRecords().get(0).getErrors()).isEmpty();
|
||||
assertThat(new QueryAction().execute(new QueryInput(DealProduct.TABLE_NAME)).getRecords().get(0).getValueBigDecimal("totalAmount")).isEqualByComparingTo("25.00");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Verify the table is registered with the correct icon.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.crm.deals;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import liquibase.Liquibase;
|
||||
import liquibase.database.jvm.JdbcConnection;
|
||||
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Upgrade an existing catalog without changing prices or product identities.
|
||||
*******************************************************************************/
|
||||
class ProductCatalogMigrationTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The real additive changeset preserves priced rows and permits unpriced
|
||||
** products while enforcing non-null slug uniqueness.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void upgradesPopulatedCatalog() throws Exception
|
||||
{
|
||||
try(Connection connection = DriverManager.getConnection("jdbc:h2:mem:catalog_" + UUID.randomUUID() + ";MODE=PostgreSQL"))
|
||||
{
|
||||
connection.createStatement().execute("CREATE TABLE crm_product (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(250) NOT NULL, unit_price DECIMAL(20,6) NOT NULL)");
|
||||
connection.createStatement().execute("INSERT INTO crm_product(id,name,unit_price) VALUES (42,'Existing product',77.25)");
|
||||
try(Liquibase liquibase = new Liquibase("db/liquibase/qbit-crm/crm-product-catalog.changelog.sql",
|
||||
new ClassLoaderResourceAccessor(), new JdbcConnection(connection)))
|
||||
{
|
||||
liquibase.update();
|
||||
liquibase.update();
|
||||
try(var result = connection.createStatement().executeQuery("SELECT id,unit_price FROM crm_product"))
|
||||
{
|
||||
assertThat(result.next()).isTrue();
|
||||
assertThat(result.getInt("id")).isEqualTo(42);
|
||||
assertThat(result.getBigDecimal("unit_price")).isEqualByComparingTo("77.25");
|
||||
}
|
||||
connection.createStatement().execute("INSERT INTO crm_product(name,slug) VALUES ('Informational','info')");
|
||||
connection.createStatement().execute("INSERT INTO crm_product(name) VALUES ('No slug one'),('No slug two')");
|
||||
assertThatThrownBy(() -> connection.createStatement().execute("INSERT INTO crm_product(name,slug) VALUES ('Duplicate','info')"))
|
||||
.isInstanceOf(SQLException.class);
|
||||
try(var result = connection.createStatement().executeQuery("SELECT unit_price FROM crm_product WHERE slug='info'"))
|
||||
{
|
||||
assertThat(result.next()).isTrue();
|
||||
assertThat(result.getBigDecimal(1)).isNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,28 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class ProductTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** An informational catalog product has no invented price; slugs are unique.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testUnpricedCatalogProductAndSlug() throws QException
|
||||
{
|
||||
InsertOutput first = new InsertAction().execute(new InsertInput(Product.TABLE_NAME)
|
||||
.withRecordEntity(new Product().withName("Documentation toolkit").withSlug("toolkit")));
|
||||
assertThat(first.getRecords().get(0).getErrors()).isEmpty();
|
||||
Product stored = new Product(new QueryAction().execute(new QueryInput(Product.TABLE_NAME)).getRecords().get(0));
|
||||
assertThat(stored.getSlug()).isEqualTo("toolkit");
|
||||
assertThat(stored.getUnitPrice()).isNull();
|
||||
InsertOutput duplicate = new InsertAction().execute(new InsertInput(Product.TABLE_NAME)
|
||||
.withRecordEntity(new Product().withName("Other toolkit").withSlug("toolkit")));
|
||||
assertThat(duplicate.getRecords().get(0).getErrors()).isNotEmpty();
|
||||
InsertOutput optional = new InsertAction().execute(new InsertInput(Product.TABLE_NAME)
|
||||
.withRecords(List.of(new Product().withName("No slug one").toQRecord(), new Product().withName("No slug two").toQRecord())));
|
||||
assertThat(optional.getRecords()).allSatisfy(record -> assertThat(record.getErrors()).isEmpty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Verify the table and PVS are registered.
|
||||
*******************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user