Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b2589b1d5
|
No files matched your search
@@ -105,12 +105,6 @@
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- tests only: the local demo server (src/test/.../demo/MarketingDemoServer) -->
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
|
||||
@@ -19,10 +19,13 @@ package com.kingsrook.qbits.marketing.widgets;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.marketing.MarketingRuntime;
|
||||
import com.kingsrook.qbits.marketing.MarketingStore;
|
||||
@@ -66,6 +69,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -91,6 +95,8 @@ public final class MarketingWidgets
|
||||
public static final String CAMPAIGN_RESULTS = "marketing_campaign_results";
|
||||
public static final String CONTENT_PROPOSAL = "marketing_content_proposal";
|
||||
|
||||
private static final DateTimeFormatter WHEN_FORMAT = DateTimeFormatter.ofPattern("MMM d, yyyy h:mm a 'UTC'", Locale.US).withZone(ZoneOffset.UTC);
|
||||
|
||||
public static final List<String> HOME_WIDGETS = List.of(NEEDS_ATTENTION, CAMPAIGNS, AGENT_ACTIVITY, CONFIGURATION);
|
||||
|
||||
|
||||
@@ -196,6 +202,17 @@ public final class MarketingWidgets
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A date-time for people: "Oct 14, 2026 4:53 PM UTC" (empty when null).
|
||||
*******************************************************************************/
|
||||
static String when(Object value)
|
||||
{
|
||||
Instant instant = ValueUtils.getValueAsInstant(value);
|
||||
return (instant == null ? "" : WHEN_FORMAT.format(instant));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@@ -242,27 +259,27 @@ public final class MarketingWidgets
|
||||
for(QRecord r : MarketingStore.query(MarketingContent.TABLE_NAME, new QQueryFilter(new QFilterCriteria("status", QCriteriaOperator.EQUALS, ContentStatus.IN_REVIEW.getId())).withLimit(25)))
|
||||
{
|
||||
rows.add(row("item", link(MarketingContent.TABLE_NAME, r.getValue("id"), r.getValueString("title")), "what", "Waiting for review (revision " + r.getValueInteger("revisionNumber") + ")",
|
||||
"since", String.valueOf(r.getValue("modifyDate"))));
|
||||
"since", when(r.getValue("modifyDate"))));
|
||||
}
|
||||
for(QRecord r : MarketingStore.query(MarketingExecution.TABLE_NAME, new QQueryFilter(new QFilterCriteria("status", QCriteriaOperator.IN,
|
||||
List.of(ExecutionStatus.BLOCKED.getId(), ExecutionStatus.NEEDS_RECONCILIATION.getId(), ExecutionStatus.FAILED.getId()))).withLimit(25)))
|
||||
{
|
||||
rows.add(row("item", link(MarketingExecution.TABLE_NAME, r.getValue("id"), "Execution #" + r.getValue("id")),
|
||||
"what", ExecutionStatus.getById(r.getValueInteger("status")).getLabel() + ": " + StringUtils.safeTruncate(r.getValueString("statusDetail"), 160, "..."),
|
||||
"since", String.valueOf(r.getValue("modifyDate"))));
|
||||
"since", when(r.getValue("modifyDate"))));
|
||||
}
|
||||
for(QRecord r : MarketingStore.query(MarketingFinding.TABLE_NAME, new QQueryFilter(
|
||||
new QFilterCriteria("status", QCriteriaOperator.EQUALS, FindingStatus.OPEN.getId()),
|
||||
new QFilterCriteria("severity", QCriteriaOperator.EQUALS, FindingSeverity.BLOCKER.getId())).withLimit(25)))
|
||||
{
|
||||
rows.add(row("item", link(MarketingFinding.TABLE_NAME, r.getValue("id"), r.getValueString("title")), "what", "Open blocker finding", "since", String.valueOf(r.getValue("createDate"))));
|
||||
rows.add(row("item", link(MarketingFinding.TABLE_NAME, r.getValue("id"), r.getValueString("title")), "what", "Open blocker finding", "since", when(r.getValue("createDate"))));
|
||||
}
|
||||
for(QRecord r : MarketingStore.query(MarketingAgentRun.TABLE_NAME, new QQueryFilter(
|
||||
new QFilterCriteria("status", QCriteriaOperator.EQUALS, AgentRunStatus.FAILED.getId()),
|
||||
new QFilterCriteria("createDate", QCriteriaOperator.GREATER_THAN, Instant.now().minus(7, ChronoUnit.DAYS))).withLimit(10)))
|
||||
{
|
||||
rows.add(row("item", link(MarketingAgentRun.TABLE_NAME, r.getValue("id"), "Agent run #" + r.getValue("id")),
|
||||
"what", "Failed: " + StringUtils.safeTruncate(r.getValueString("errorMessage"), 160, "..."), "since", String.valueOf(r.getValue("createDate"))));
|
||||
"what", "Failed: " + StringUtils.safeTruncate(r.getValueString("errorMessage"), 160, "..."), "since", when(r.getValue("createDate"))));
|
||||
}
|
||||
|
||||
return (new RenderWidgetOutput(table(List.of(column("Item", "item", true), column("What", "what", false), column("Since", "since", false)), rows,
|
||||
@@ -438,9 +455,25 @@ public final class MarketingWidgets
|
||||
new QFilterCriteria("contentId", QCriteriaOperator.EQUALS, contentId),
|
||||
new QFilterCriteria("status", QCriteriaOperator.EQUALS, ApprovalStatus.ACTIVE.getId())));
|
||||
|
||||
String approval = active.isEmpty() ? "Not approved."
|
||||
: "Approved by " + escape(active.get(0).getValueString("approvedByUserId")) + " at " + active.get(0).getValue("approvedAt")
|
||||
String approval;
|
||||
if(!active.isEmpty())
|
||||
{
|
||||
approval = "Approved by " + escape(active.get(0).getValueString("approvedByUserId")) + " on " + when(active.get(0).getValue("approvedAt"))
|
||||
+ (content.getContentHash() != null && content.getContentHash().equals(active.get(0).getValueString("contentHash")) ? " (matches this revision)." : " (does NOT match this revision).");
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// an approval an execution used up is not "not approved": say so //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> consumed = MarketingStore.query(MarketingApproval.TABLE_NAME, new QQueryFilter(
|
||||
new QFilterCriteria("contentId", QCriteriaOperator.EQUALS, contentId),
|
||||
new QFilterCriteria("status", QCriteriaOperator.EQUALS, ApprovalStatus.CONSUMED.getId()),
|
||||
new QFilterCriteria("contentHash", QCriteriaOperator.EQUALS, content.getContentHash())));
|
||||
approval = consumed.isEmpty() ? "Not approved."
|
||||
: "Approved by " + escape(consumed.get(0).getValueString("approvedByUserId")) + " on " + when(consumed.get(0).getValue("approvedAt"))
|
||||
+ ", and used by an execution. Approve again to send this revision again.";
|
||||
}
|
||||
|
||||
StringBuilder html = new StringBuilder("<div style=\"max-width:100%;overflow-wrap:anywhere\">");
|
||||
html.append("<p><strong>").append(escape(content.getTitle())).append("</strong></p>");
|
||||
@@ -450,7 +483,7 @@ public final class MarketingWidgets
|
||||
item(html, "Call to action", content.getCallToAction());
|
||||
item(html, "Destination", content.getDestinationUrl());
|
||||
item(html, "Recipients", StringUtils.hasContent(content.getRecipientScope()) ? content.getRecipientScope() : "public (no recipient list)");
|
||||
item(html, "Scheduled for", content.getScheduledFor() == null ? "(not set)" : content.getScheduledFor().toString());
|
||||
item(html, "Scheduled for", content.getScheduledFor() == null ? "(not set)" : when(content.getScheduledFor()));
|
||||
item(html, "Revision", content.getRevisionNumber() + " by " + label(AuthorType.getById(content.getAuthorType())));
|
||||
item(html, "Content hash", content.getContentHash() == null ? "" : content.getContentHash().substring(0, 12));
|
||||
item(html, "Status", label(ContentStatus.getById(content.getStatus())));
|
||||
|
||||
@@ -253,6 +253,13 @@ class EndToEndFlowTest extends BaseMarketingTest
|
||||
assertThat(deliveries.get(0).getValueString("body")).as("exactly the approved text").isEqualTo(body);
|
||||
assertThat(reload(MarketingApproval.TABLE_NAME, approval.getValueInteger("id")).getValueInteger("status")).isEqualTo(ApprovalStatus.CONSUMED.getId());
|
||||
assertThat(reload(MarketingContent.TABLE_NAME, contentId).getValueInteger("status")).isEqualTo(ContentStatus.ACCEPTED.getId());
|
||||
com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput proposalInput = new com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput()
|
||||
.withWidgetMetaData(com.kingsrook.qqq.backend.core.context.QContext.getQInstance().getWidget(com.kingsrook.qbits.marketing.widgets.MarketingWidgets.CONTENT_PROPOSAL));
|
||||
proposalInput.setQueryParams(new java.util.HashMap<>(java.util.Map.of("id", String.valueOf(contentId))));
|
||||
String proposal = ((com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML) new com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction()
|
||||
.execute(proposalInput).getWidgetData()).getHtml();
|
||||
assertThat(proposal).as("a used-up approval is not shown as not approved").contains("used by an execution").doesNotContain("Not approved")
|
||||
.containsPattern("[A-Z][a-z]{2} [0-9]{1,2}, [0-9]{4} [0-9]{1,2}:[0-9]{2} [AP]M UTC");
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// 7. recorded outcomes: the provider acceptance (test data) and //
|
||||
|
||||
@@ -38,7 +38,7 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
/*******************************************************************************
|
||||
** Synchronous scripted responses and request snapshots for marketing tests.
|
||||
*******************************************************************************/
|
||||
public class ScriptedModelProvider implements AssistantModelProvider
|
||||
class ScriptedModelProvider implements AssistantModelProvider
|
||||
{
|
||||
private final String name;
|
||||
private final Deque<Step> steps = new ArrayDeque<>();
|
||||
@@ -51,7 +51,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
/*******************************************************************************
|
||||
** Register under the name selected by the test configuration.
|
||||
*******************************************************************************/
|
||||
public ScriptedModelProvider(String name)
|
||||
ScriptedModelProvider(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
/*******************************************************************************
|
||||
** Queue a complete answer.
|
||||
*******************************************************************************/
|
||||
public ScriptedModelProvider thenText(String text)
|
||||
ScriptedModelProvider thenText(String text)
|
||||
{
|
||||
return (thenRespond(request -> new ModelResponse().withParts(List.of(ModelPart.text(text))).withStopReason(ModelResponse.STOP_END_TURN)));
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
/*******************************************************************************
|
||||
** Queue a single tool call.
|
||||
*******************************************************************************/
|
||||
public ScriptedModelProvider thenToolUse(String toolName, Map<String, Object> input)
|
||||
ScriptedModelProvider thenToolUse(String toolName, Map<String, Object> input)
|
||||
{
|
||||
return (thenToolUses(null, List.<Object[]>of(new Object[] { toolName, input })));
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
** Queue parallel tool calls using the test fixtures' name/input pairs.
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ScriptedModelProvider thenToolUses(String preamble, List<Object[]> calls)
|
||||
ScriptedModelProvider thenToolUses(String preamble, List<Object[]> calls)
|
||||
{
|
||||
return (thenRespond(request ->
|
||||
{
|
||||
@@ -104,7 +104,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
/*******************************************************************************
|
||||
** Queue a provider failure, including usage already incurred.
|
||||
*******************************************************************************/
|
||||
public ScriptedModelProvider thenError(ModelProviderException.Kind kind, String message)
|
||||
ScriptedModelProvider thenError(ModelProviderException.Kind kind, String message)
|
||||
{
|
||||
return (thenRespond(request ->
|
||||
{
|
||||
@@ -117,7 +117,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
/*******************************************************************************
|
||||
** Queue a response that can inspect the request or change test state.
|
||||
*******************************************************************************/
|
||||
public ScriptedModelProvider thenRespond(Step step)
|
||||
ScriptedModelProvider thenRespond(Step step)
|
||||
{
|
||||
steps.add(step);
|
||||
return (this);
|
||||
@@ -128,7 +128,7 @@ public class ScriptedModelProvider implements AssistantModelProvider
|
||||
/*******************************************************************************
|
||||
** Set usage for budget and cost assertions.
|
||||
*******************************************************************************/
|
||||
public ScriptedModelProvider withUsagePerCall(ModelUsage usage)
|
||||
ScriptedModelProvider withUsagePerCall(ModelUsage usage)
|
||||
{
|
||||
usagePerCall = usage;
|
||||
return (this);
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.marketing.demo;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.marketing.MarketingQBitConfig;
|
||||
import com.kingsrook.qbits.marketing.MarketingQBitProducer;
|
||||
import com.kingsrook.qbits.marketing.MarketingStore;
|
||||
import com.kingsrook.qbits.marketing.ScriptedModelProvider;
|
||||
import com.kingsrook.qbits.marketing.agent.MarketingAgentService;
|
||||
import com.kingsrook.qbits.marketing.agent.tools.MarketingTools;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingCampaign;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingChannel;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingClaim;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingContent;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingOutcomeEvent;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingProduct;
|
||||
import com.kingsrook.qbits.marketing.model.enums.AgentTask;
|
||||
import com.kingsrook.qbits.marketing.model.enums.ClaimStatus;
|
||||
import com.kingsrook.qbits.marketing.model.enums.FunnelStage;
|
||||
import com.kingsrook.qbits.marketing.processes.MarketingActions;
|
||||
import com.kingsrook.qbits.marketing.processes.MarketingTableProcesses;
|
||||
import com.kingsrook.qbits.marketing.security.MarketingPermissions;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A local demo of the Marketing qbit for screenshots and manual checks (not a
|
||||
** test, not shipped): an in-memory H2 database created from the shipped DDL
|
||||
** (src/test/resources/marketing-h2.sql), a mock login as a user holding every
|
||||
** marketing permission, the verified starter data, and some history made
|
||||
** through the real services: a readiness check, an agent draft (offline,
|
||||
** scripted model), review, approval, scheduling, a controlled test execution
|
||||
** with its attempt and test delivery, and a few outcome events.
|
||||
**
|
||||
** Run it (from the repository root), then the Next UI dev server against it:
|
||||
**
|
||||
** ./mvnw -q test-compile
|
||||
** ./mvnw -q dependency:build-classpath -Dmdep.outputFile=target/cp.txt -Dmdep.includeScope=test
|
||||
** java -cp "target/test-classes;target/classes;$(cat target/cp.txt)" com.kingsrook.qbits.marketing.demo.MarketingDemoServer
|
||||
**
|
||||
** (Use ':' instead of ';' in the classpath on Linux/macOS. The server listens
|
||||
** on port 8000, or MARKETING_DEMO_PORT.)
|
||||
**
|
||||
** QQQ_BACKEND_URL=http://localhost:8000 pnpm dev (in qqq-frontend-next; port 3000)
|
||||
*******************************************************************************/
|
||||
public class MarketingDemoServer extends AbstractQQQApplication
|
||||
{
|
||||
static final String BACKEND = "demo";
|
||||
static final String USER = "dana";
|
||||
|
||||
private static final String BODY = "QQQ 4.0 is on Maven Central: you can try it with the quickstart in a few minutes. Java 21, Docker and Bash are required.";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Start the demo.
|
||||
*******************************************************************************/
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
MarketingDemoServer application = new MarketingDemoServer();
|
||||
new QApplicationJavalinServer(application)
|
||||
.withServeFrontendNext(false)
|
||||
.withServeFrontendMaterialDashboard(false)
|
||||
.withPort(Integer.parseInt(System.getenv().getOrDefault("MARKETING_DEMO_PORT", "8000")))
|
||||
.start();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The demo instance (seeded on definition).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QInstance defineQInstance() throws QException
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
RDBMSBackendMetaData backend = new RDBMSBackendMetaData().withName(BACKEND).withVendor("h2").withHostName("mem").withDatabaseName("marketing_demo").withUsername("sa");
|
||||
backend.setJdbcUrl(ConnectionManager.getJdbcUrl(backend) + ";DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1");
|
||||
qInstance.addBackend(backend);
|
||||
qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK)
|
||||
.withCustomizer(new QCodeReference(DemoSessionCustomizer.class)));
|
||||
qInstance.setBranding(new QBrandingMetaData().withAppName("Marketing demo").withCompanyName("QRun-IO"));
|
||||
qInstance.setEnforcePermissionsInCoreActions(true);
|
||||
|
||||
createTables(backend);
|
||||
|
||||
ScriptedModelProvider model = new ScriptedModelProvider("scripted").withUsagePerCall(new ModelUsage().withInputTokens(1200L).withOutputTokens(300L));
|
||||
new MarketingQBitProducer().withQBitConfig(new MarketingQBitConfig().withBackendName(BACKEND).withModelProvider(model).withModel("scripted-model")).produce(qInstance);
|
||||
|
||||
seed(qInstance, model);
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Create the tables from the shipped H2 DDL.
|
||||
*******************************************************************************/
|
||||
static void createTables(RDBMSBackendMetaData backend) throws QException
|
||||
{
|
||||
try(Connection connection = ConnectionManager.getConnection(backend); Statement statement = connection.createStatement();
|
||||
InputStream ddl = MarketingDemoServer.class.getResourceAsStream("/marketing-h2.sql"))
|
||||
{
|
||||
String sql = new String(ddl.readAllBytes(), StandardCharsets.UTF_8);
|
||||
for(String part : sql.split(";"))
|
||||
{
|
||||
String trimmed = part.lines().filter(l -> !l.trim().startsWith("--")).reduce("", (a, b) -> a + "\n" + b).trim();
|
||||
if(!trimmed.isEmpty())
|
||||
{
|
||||
statement.execute(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Could not create the demo tables", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Starter data, then history through the real services and processes.
|
||||
*******************************************************************************/
|
||||
static void seed(QInstance qInstance, ScriptedModelProvider model) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
QContext.init(qInstance, DemoSessionCustomizer.sessionFor(USER));
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// the qbit's own "Load Verified Starter Data" table action //
|
||||
////////////////////////////////////////////////////////////////
|
||||
requireNoException(new RunProcessAction().execute(new RunProcessInput().withProcessName(MarketingTableProcesses.LOAD_STARTER_DATA)
|
||||
.withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP)), "starter data");
|
||||
|
||||
QRecord product = first(MarketingProduct.TABLE_NAME, "slug", "qqq");
|
||||
QRecord campaign = first(MarketingCampaign.TABLE_NAME, "name", "QQQ 4.0: from quickstart to your first table");
|
||||
QRecord sink = first(MarketingChannel.TABLE_NAME, "name", "Controlled test sink");
|
||||
Integer campaignId = campaign.getValueInteger("id");
|
||||
|
||||
requireNoException(runAction(MarketingActions.CHECK_CAMPAIGN_READINESS, campaignId), "campaign readiness");
|
||||
requireNoException(runAction(MarketingActions.CHECK_CAMPAIGN_READINESS, first(MarketingCampaign.TABLE_NAME, "name", "gclo 1.1 for Windows: mirror a whole GitHub org").getValue("id")), "campaign readiness");
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Draft with Agent, offline: the scripted model reads, then saves a draft //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
Integer released = MarketingStore.query(MarketingClaim.TABLE_NAME, new QQueryFilter(
|
||||
new QFilterCriteria("productId", QCriteriaOperator.EQUALS, product.getValue("id")),
|
||||
new QFilterCriteria("status", QCriteriaOperator.EQUALS, ClaimStatus.RELEASED.getId()))).get(0).getValueInteger("id");
|
||||
model.thenToolUses("Reading the brief first.", List.of(
|
||||
new Object[] { MarketingTools.GET_CAMPAIGN_BRIEF, Map.of("campaign_id", campaignId) },
|
||||
new Object[] { MarketingTools.LIST_PRODUCT_CLAIMS, Map.of("product_id", product.getValueInteger("id")) },
|
||||
new Object[] { MarketingTools.LIST_CHANNELS, Map.of() }))
|
||||
.thenToolUse(MarketingTools.SAVE_DRAFT, Map.of("campaign_id", campaignId, "channel_id", sink.getValueInteger("id"), "title", "Try QQQ 4.0 in a few minutes",
|
||||
"body", BODY, "claim_ids", List.of(released)))
|
||||
.thenText("I saved one draft for review on the controlled test sink.");
|
||||
new MarketingAgentService().run(AgentTask.DRAFT_CONTENT, campaignId, null, null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// a person schedules it, submits it, approves it and schedules it; the //
|
||||
// worker then runs the due execution on the controlled test sink //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
Integer contentId = first(MarketingContent.TABLE_NAME, "campaignId", campaignId).getValueInteger("id");
|
||||
MarketingStore.update(MarketingContent.TABLE_NAME, new QRecord().withValue("id", contentId).withValue("scheduledFor", Instant.now().minusSeconds(5)));
|
||||
requireNoException(runAction(MarketingActions.SUBMIT_FOR_REVIEW, contentId), "submit for review");
|
||||
requireNoException(runAction(MarketingActions.APPROVE, contentId), "approve");
|
||||
requireNoException(runAction(MarketingActions.SCHEDULE, contentId), "schedule");
|
||||
requireNoException(new RunProcessAction().execute(new RunProcessInput().withProcessName(MarketingTableProcesses.RUN_DUE_EXECUTIONS)
|
||||
.withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP)), "run due executions");
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// a second piece of content, waiting for review //
|
||||
///////////////////////////////////////////////////
|
||||
Integer waiting = MarketingStore.insert(new MarketingContent().withCampaignId(campaignId).withChannelId(sink.getValueInteger("id")).withTitle("Your first table, step by step")
|
||||
.withBody("The quickstart takes you from an empty project to your first table. Java 21, Docker and Bash are required.")
|
||||
.withScheduledFor(Instant.now().plusSeconds(3600L * 24))).getValueInteger("id");
|
||||
requireNoException(runAction(MarketingActions.SUBMIT_FOR_REVIEW, waiting), "submit for review");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// controlled fixture events (test data, never counted as real numbers) //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
for(int i = 1; i <= 5; i++)
|
||||
{
|
||||
MarketingStore.insert(new MarketingOutcomeEvent().withCampaignId(campaignId).withStage(FunnelStage.AWARENESS.getId()).withEventType("try_page_visit")
|
||||
.withSource("fixture").withExternalEventId("visit-" + i).withActorKey("visitor-" + (i % 3)).withIsTestData(true));
|
||||
}
|
||||
MarketingStore.insert(new MarketingOutcomeEvent().withCampaignId(campaignId).withStage(FunnelStage.ENGAGEMENT.getId()).withEventType("quickstart_opened")
|
||||
.withSource("fixture").withExternalEventId("open-1").withActorKey("visitor-1").withIsTestData(true));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The first record matching one field.
|
||||
*******************************************************************************/
|
||||
private static QRecord first(String table, String field, Serializable value) throws QException
|
||||
{
|
||||
return (MarketingStore.queryBy(table, field, value).get(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run a record action headlessly on one record.
|
||||
*******************************************************************************/
|
||||
private static RunProcessOutput runAction(String processName, Serializable id) throws QException
|
||||
{
|
||||
return (new RunProcessAction().execute(new RunProcessInput().withProcessName(processName).withCallback(QProcessCallbackFactory.forPrimaryKey("id", id))
|
||||
.withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fail the demo start-up if a seeding step failed.
|
||||
*******************************************************************************/
|
||||
private static void requireNoException(RunProcessOutput output, String what) throws QException
|
||||
{
|
||||
if(output.getException().isPresent())
|
||||
{
|
||||
throw (new QException("Demo seeding failed at [" + what + "]", output.getException().get()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every mock session is Dana, who holds every marketing permission.
|
||||
*******************************************************************************/
|
||||
public static class DemoSessionCustomizer implements QAuthenticationModuleCustomizerInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void customizeSession(QInstance qInstance, QSession qSession, Map<String, Object> context)
|
||||
{
|
||||
QSession granted = sessionFor(USER);
|
||||
qSession.setUser(granted.getUser());
|
||||
qSession.setPermissions(granted.getPermissions());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void customizeAutomatedSessionForUser(QInstance qInstance, QSession automatedSessionForUser, Serializable userId)
|
||||
{
|
||||
automatedSessionForUser.setPermissions(sessionFor(String.valueOf(userId)).getPermissions());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Dana's session.
|
||||
***************************************************************************/
|
||||
static QSession sessionFor(String userId)
|
||||
{
|
||||
QSession session = new QSession().withUser(new QUser().withIdReference(userId).withFullName("Dana Demo"));
|
||||
session.setPermissions(new java.util.HashSet<>(MarketingPermissions.ALL));
|
||||
return (session);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user