@nextcommerce/campaigns-os 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- package/src/upsell-selector-scope.mjs +299 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Cart entry for the typed-card runner (campaigns-os#206, runner half).
|
|
2
|
+
//
|
|
3
|
+
// A checkout page renders its customer form whether or not the SDK cart holds
|
|
4
|
+
// anything, so a runner that opens the checkout URL directly cannot tell a
|
|
5
|
+
// "select here" checkout (Demeter, Apollo, Olympus: the bundle cards live on
|
|
6
|
+
// the checkout page) from a "cart was filled upstream" checkout (the
|
|
7
|
+
// shop-single-step family: the landing page adds to the cart and hands off).
|
|
8
|
+
// On the second shape the ladder used to run every fill step green and then sit
|
|
9
|
+
// in `order_submitted` for the full step budget, because the SDK never posts an
|
|
10
|
+
// order for an empty cart and the captured request log had nothing to say.
|
|
11
|
+
//
|
|
12
|
+
// This module holds the browser-free half of the fix: resolving which page the
|
|
13
|
+
// funnel enters the cart from, the DOM probes (as evaluate() bodies) that
|
|
14
|
+
// decide whether a checkout carries its own selection surface, the SDK cart
|
|
15
|
+
// read used by the pre-submit guard, and the named failure codes the ladder
|
|
16
|
+
// reports instead of a timeout. The design half of #206 — authoritative entry
|
|
17
|
+
// metadata on the spec — is deliberately NOT decided here: entry is still
|
|
18
|
+
// inferred from topology, and the inference is recorded on the ladder step so a
|
|
19
|
+
// reader can see which page the runner chose and why.
|
|
20
|
+
|
|
21
|
+
import { OFFER_PAGE_TYPES, RECEIPT_PAGE_TYPES, canonicalHttpUrl } from "./qa-test-order-topology.mjs";
|
|
22
|
+
|
|
23
|
+
// Ladder step recorded before `opened_checkout`.
|
|
24
|
+
export const CART_ENTRY_STEP = "entered_via_landing";
|
|
25
|
+
|
|
26
|
+
// Named failure codes. Each one is a pre-submit failure by construction: the
|
|
27
|
+
// runner authored the refusal before any reservation and before any click, so
|
|
28
|
+
// the creation classifier reads them as `not_created`.
|
|
29
|
+
export const CART_ENTRY_CODES = Object.freeze({
|
|
30
|
+
// Checkout carries no selection surface, and no landing/entry page could be
|
|
31
|
+
// resolved from the funnel topology to fill the cart from.
|
|
32
|
+
ENTRY_UNRESOLVED: "cart_entry_unresolved",
|
|
33
|
+
// The resolved entry page rendered no add-to-cart control (or none matching
|
|
34
|
+
// the requested --select-package ref).
|
|
35
|
+
ENTRY_CONTROL_MISSING: "cart_entry_control_missing",
|
|
36
|
+
// The control was clicked but the page never reached the checkout URL.
|
|
37
|
+
ENTRY_NO_NAVIGATION: "cart_entry_no_navigation",
|
|
38
|
+
// The SDK cart held zero items at submit time. Fired before the budget
|
|
39
|
+
// reservation and before the submit click.
|
|
40
|
+
CART_EMPTY_BEFORE_SUBMIT: "cart_empty_before_submit",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// The SDK's own activation selector for its add-to-cart feature, and nothing
|
|
44
|
+
// else: the SDK instantiates the feature on `data-next-action="add-to-cart"`
|
|
45
|
+
// only, so any other attribute spelling is a control the SDK never wires. A
|
|
46
|
+
// page carrying one of those must read as "no cart-entry control", not as an
|
|
47
|
+
// entry the runner can click, or the ladder claims an activation the SDK
|
|
48
|
+
// does not have.
|
|
49
|
+
export const CART_ENTRY_CONTROL_SELECTOR = '[data-next-action="add-to-cart"]';
|
|
50
|
+
|
|
51
|
+
// The attribute an SDK cart-entry control (one matching
|
|
52
|
+
// CART_ENTRY_CONTROL_SELECTOR) navigates by: the SDK adds the package and then
|
|
53
|
+
// goes to `data-next-url` itself, so such a control carries no href. Only SDK
|
|
54
|
+
// controls get this reading — on any other element `data-next-url` has no
|
|
55
|
+
// navigation semantics and the href-shaped attributes stay authoritative
|
|
56
|
+
// (campaigns-os#321: the primary-CTA recogniser used to read href-shaped
|
|
57
|
+
// attributes only, so the SDK's own button on a landing page counted as "no
|
|
58
|
+
// CTA to the next route" while a plain cart-bypassing <a href> passed). The
|
|
59
|
+
// ladder's entry step and the primary-CTA assertion share this one rule.
|
|
60
|
+
export const CART_ENTRY_ROUTE_ATTRIBUTE = "data-next-url";
|
|
61
|
+
|
|
62
|
+
// The route a candidate CTA leads to, as one pure rule shared by the
|
|
63
|
+
// primary-CTA recogniser (which runs it inside the page) and the unit tests
|
|
64
|
+
// (which run it against element-shaped objects). Self-contained on purpose:
|
|
65
|
+
// the browser half serialises this function's source into the page, so it
|
|
66
|
+
// may reference nothing outside its own body.
|
|
67
|
+
//
|
|
68
|
+
// An SDK cart-entry control (one matching `cartEntrySelector`) navigates by
|
|
69
|
+
// `cartEntryRouteAttribute` and nothing else: the SDK's click handler calls
|
|
70
|
+
// preventDefault() unconditionally, so its href never navigates, and it
|
|
71
|
+
// resolves the attribute against the origin (campaign-cart url-utils), not the
|
|
72
|
+
// document base. Without the attribute such a control adds to the cart and
|
|
73
|
+
// stays put — no route. Anywhere else the SDK attribute has no navigation
|
|
74
|
+
// semantics: an HTML anchor's own resolved href (native, so a <base href> is
|
|
75
|
+
// honoured), then an href attribute, then `data-href`, then a wrapping form's
|
|
76
|
+
// action. Only spellings with real navigation semantics count — an attribute
|
|
77
|
+
// the SDK does not declare is not a route.
|
|
78
|
+
export function cartEntryHrefFor(element, { cartEntrySelector, cartEntryRouteAttribute, origin, baseHref }) {
|
|
79
|
+
if (!element || typeof element.getAttribute !== "function") return null;
|
|
80
|
+
if (typeof element.matches === "function" && element.matches(cartEntrySelector)) {
|
|
81
|
+
const sdkRoute = String(element.getAttribute(cartEntryRouteAttribute) || "").trim();
|
|
82
|
+
if (!sdkRoute) return null;
|
|
83
|
+
try {
|
|
84
|
+
return new URL(sdkRoute, origin).href;
|
|
85
|
+
} catch {
|
|
86
|
+
// An unparseable route attribute is no route either; do not leak the
|
|
87
|
+
// raw value into evidence as if it were one.
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// An HTML anchor resolves its own href (an SVG <a> exposes an object, not a
|
|
92
|
+
// string, and falls through to the attribute reading).
|
|
93
|
+
if (String(element.tagName || "").toUpperCase() === "A" && typeof element.href === "string" && element.href) return element.href;
|
|
94
|
+
const attr = element.getAttribute("href")
|
|
95
|
+
|| element.getAttribute("data-href")
|
|
96
|
+
|| (typeof element.closest === "function" ? element.closest("form")?.getAttribute("action") : null);
|
|
97
|
+
if (!attr) return null;
|
|
98
|
+
try {
|
|
99
|
+
return new URL(attr, baseHref).href;
|
|
100
|
+
} catch {
|
|
101
|
+
// Same rule as the SDK branch: an unparseable attribute is not a route,
|
|
102
|
+
// and evidence `href` fields stay parseable URLs or null.
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Route-shaped spellings the runner used to consult and no longer does,
|
|
108
|
+
// because the SDK never declares them. They are not routes, but a page that
|
|
109
|
+
// carries one is reported so an operator can tell "the runner narrowed its
|
|
110
|
+
// vocabulary" from "the CTA was removed" when a missing-route verdict flips.
|
|
111
|
+
export const UNDECLARED_ROUTE_ATTRIBUTES = Object.freeze(["data-next-href", "data-next-checkout-action"]);
|
|
112
|
+
|
|
113
|
+
// Page types that, when they route into checkout, are preferred as the cart
|
|
114
|
+
// entry. `select` is a real page type since #228; `product` is what the
|
|
115
|
+
// shop-single-step landing declares; the rest are the entry-like types
|
|
116
|
+
// `deriveEntryUrls` already recognises.
|
|
117
|
+
// Pages that can only follow the checkout: the checkout itself plus the
|
|
118
|
+
// topology module's own offer and receipt sets, so the two never drift.
|
|
119
|
+
const POST_CHECKOUT_PAGE_TYPES = new Set(["checkout", ...OFFER_PAGE_TYPES, ...RECEIPT_PAGE_TYPES]);
|
|
120
|
+
const PREFERRED_ENTRY_PAGE_TYPES = ["select", "landing", "product", "presell", "entry", "lander", "advertorial", "listicle", "review", "opt-in", "optin"];
|
|
121
|
+
|
|
122
|
+
export function codedError(code, message) {
|
|
123
|
+
const error = new Error(`${code}: ${message}`);
|
|
124
|
+
error.code = code;
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isCartEntryCode(code) {
|
|
129
|
+
return Object.values(CART_ENTRY_CODES).includes(code);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pageTypeOf(page) {
|
|
133
|
+
return String(page?.page_type || "").toLowerCase().trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sameUrl(left, right) {
|
|
137
|
+
const a = canonicalHttpUrl(left);
|
|
138
|
+
const b = canonicalHttpUrl(right);
|
|
139
|
+
return Boolean(a && b && a === b);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Which page fills the cart before checkout. Resolution order:
|
|
143
|
+
// 1. a page (other than checkout) whose `expected_next_url` is the checkout,
|
|
144
|
+
// preferring the entry-like page types above, then the lowest `order`;
|
|
145
|
+
// 2. the topology's first entry-like page, else its first page — the same
|
|
146
|
+
// choice `deriveEntryUrls` makes for `entry_urls[0]` — restricted to
|
|
147
|
+
// pages that sit BEFORE the checkout. A receipt or an offer page can
|
|
148
|
+
// never be the cart entry, whatever the fallback order says.
|
|
149
|
+
// Returns null when nothing resolves; the caller turns that into a named
|
|
150
|
+
// failure only when the checkout also carries no selection surface.
|
|
151
|
+
export function resolveCartEntryPage(topologies = [], checkoutPage = null) {
|
|
152
|
+
if (!checkoutPage?.url) return null;
|
|
153
|
+
const list = Array.isArray(topologies) ? topologies : [];
|
|
154
|
+
const owning = list.find((topology) => (topology?.pages || []).some((page) => page === checkoutPage || sameUrl(page?.url, checkoutPage.url)))
|
|
155
|
+
|| list[0]
|
|
156
|
+
|| null;
|
|
157
|
+
const checkoutOrder = Number(checkoutPage.order);
|
|
158
|
+
const pages = (owning?.pages || []).filter((page) => (
|
|
159
|
+
page?.url
|
|
160
|
+
&& !sameUrl(page.url, checkoutPage.url)
|
|
161
|
+
&& !POST_CHECKOUT_PAGE_TYPES.has(pageTypeOf(page))
|
|
162
|
+
&& (!Number.isFinite(checkoutOrder) || !Number.isFinite(Number(page.order)) || Number(page.order) < checkoutOrder)
|
|
163
|
+
));
|
|
164
|
+
if (!pages.length) return null;
|
|
165
|
+
|
|
166
|
+
const rank = (page) => {
|
|
167
|
+
const index = PREFERRED_ENTRY_PAGE_TYPES.indexOf(pageTypeOf(page));
|
|
168
|
+
return index === -1 ? PREFERRED_ENTRY_PAGE_TYPES.length : index;
|
|
169
|
+
};
|
|
170
|
+
const byPreference = (left, right) => rank(left) - rank(right) || (Number(left.order) || 0) - (Number(right.order) || 0);
|
|
171
|
+
|
|
172
|
+
const routesIntoCheckout = pages.filter((page) => sameUrl(page.expected_next_url, checkoutPage.url)).sort(byPreference);
|
|
173
|
+
if (routesIntoCheckout.length) return describe(routesIntoCheckout[0], "routes_into_checkout");
|
|
174
|
+
|
|
175
|
+
const entryLike = pages.filter((page) => PREFERRED_ENTRY_PAGE_TYPES.includes(pageTypeOf(page))).sort(byPreference);
|
|
176
|
+
if (entryLike.length) return describe(entryLike[0], "entry_page_fallback");
|
|
177
|
+
return describe(pages[0], "first_page_fallback");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function describe(page, resolution) {
|
|
181
|
+
return {
|
|
182
|
+
page_id: page.page_id || null,
|
|
183
|
+
page_type: page.page_type || null,
|
|
184
|
+
url: page.url,
|
|
185
|
+
resolution,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// evaluate() body: does this checkout carry a main-cart selection surface?
|
|
190
|
+
// Counted: bundle/cart selectors and package cards. Not counted: anything
|
|
191
|
+
// inside the rendered cart summary (it displays `cart.*`, it does not select),
|
|
192
|
+
// order-bump toggles (an add-on is not the main cart), upsell-context
|
|
193
|
+
// selectors, and <template> content the SDK has not rendered.
|
|
194
|
+
export function checkoutSelectionSurfaceScript() {
|
|
195
|
+
return () => {
|
|
196
|
+
const EXCLUDED_ANCESTORS = "[data-next-cart-summary], [data-next-toggle-card], [data-next-package-toggle], [data-next-bump], [data-next-upsell-context], [data-next-upsell], template";
|
|
197
|
+
const candidates = Array.from(document.querySelectorAll("[data-next-bundle-selector], [data-next-cart-selector], [data-next-package-id]"));
|
|
198
|
+
const counted = candidates.filter((element) => !element.closest(EXCLUDED_ANCESTORS));
|
|
199
|
+
const kinds = {
|
|
200
|
+
bundle_selector: counted.filter((element) => element.hasAttribute("data-next-bundle-selector")).length,
|
|
201
|
+
cart_selector: counted.filter((element) => element.hasAttribute("data-next-cart-selector")).length,
|
|
202
|
+
package_card: counted.filter((element) => element.hasAttribute("data-next-package-id")).length,
|
|
203
|
+
};
|
|
204
|
+
return { count: counted.length, kinds, excluded: candidates.length - counted.length };
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function summarizeSelectionSurface(surface) {
|
|
209
|
+
const kinds = surface?.kinds || {};
|
|
210
|
+
const parts = Object.entries(kinds).filter(([, count]) => count > 0).map(([kind, count]) => `${count} ${kind}`);
|
|
211
|
+
return parts.length ? parts.join(", ") : "none";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// evaluate() body: the cart-entry controls the entry page renders. Two kinds:
|
|
215
|
+
// add_to_cart the SDK action controls (CART_ENTRY_CONTROL_SELECTOR); the
|
|
216
|
+
// SDK adds the package and navigates via data-next-url;
|
|
217
|
+
// checkout_link a plain link into the checkout URL carrying
|
|
218
|
+
// `?forcePackageId=`, which the SDK reads on the checkout page
|
|
219
|
+
// to pre-load the cart. This is what the certified
|
|
220
|
+
// shop-single-step landing renders today.
|
|
221
|
+
// Each control reports its visibility, text, and the package id it carries
|
|
222
|
+
// (own attribute, nearest card, or the forcePackageId ref), so the caller can
|
|
223
|
+
// prefer a visible SDK control and honour --select-package in one round trip.
|
|
224
|
+
export function cartEntryControlsScript() {
|
|
225
|
+
return ({ selector, checkoutUrl }) => {
|
|
226
|
+
const clean = (value) => String(value || "").replace(/\s+/g, " ").trim();
|
|
227
|
+
const isVisible = (element) => {
|
|
228
|
+
const rect = element.getBoundingClientRect();
|
|
229
|
+
const style = getComputedStyle(element);
|
|
230
|
+
return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
|
|
231
|
+
};
|
|
232
|
+
const canonical = (value) => {
|
|
233
|
+
try {
|
|
234
|
+
const url = new URL(value, document.baseURI);
|
|
235
|
+
url.search = "";
|
|
236
|
+
url.hash = "";
|
|
237
|
+
url.pathname = url.pathname.replace(/\/index\.html$/i, "/").replace(/\/+$/, "") || "/";
|
|
238
|
+
return url.toString();
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const label = (element) => clean(element.textContent).slice(0, 80) || clean(element.getAttribute("aria-label")) || clean(element.getAttribute("value")) || null;
|
|
244
|
+
const readPackageId = (element) => {
|
|
245
|
+
const own = element.getAttribute("data-next-package-id");
|
|
246
|
+
if (own && own.trim()) return own.trim();
|
|
247
|
+
const card = element.closest("[data-next-package-id]");
|
|
248
|
+
return card ? clean(card.getAttribute("data-next-package-id")) || null : null;
|
|
249
|
+
};
|
|
250
|
+
const readQuantity = (value) => {
|
|
251
|
+
const parsed = Number.parseInt(String(value || "").trim(), 10);
|
|
252
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
|
253
|
+
};
|
|
254
|
+
// `index` is the control's position among the elements its own locator
|
|
255
|
+
// matches (the SDK-control selector, or every `a[href]`), so the caller
|
|
256
|
+
// can replay it with nth() instead of rebuilding a selector from the
|
|
257
|
+
// attribute value.
|
|
258
|
+
const actions = Array.from(document.querySelectorAll(selector)).map((element, index) => ({
|
|
259
|
+
kind: "add_to_cart",
|
|
260
|
+
index,
|
|
261
|
+
visible: isVisible(element),
|
|
262
|
+
text: label(element),
|
|
263
|
+
package_id: readPackageId(element),
|
|
264
|
+
quantity: readQuantity(element.getAttribute("data-next-quantity")),
|
|
265
|
+
next_url: clean(element.getAttribute("data-next-url")) || null,
|
|
266
|
+
tag: element.tagName.toLowerCase(),
|
|
267
|
+
}));
|
|
268
|
+
const checkout = canonical(checkoutUrl);
|
|
269
|
+
const links = checkout
|
|
270
|
+
? Array.from(document.querySelectorAll("a[href]")).flatMap((element, index) => {
|
|
271
|
+
const href = element.getAttribute("href");
|
|
272
|
+
if (canonical(href) !== checkout) return [];
|
|
273
|
+
let forced = null;
|
|
274
|
+
try {
|
|
275
|
+
forced = new URL(href, document.baseURI).searchParams.get("forcePackageId");
|
|
276
|
+
} catch {
|
|
277
|
+
forced = null;
|
|
278
|
+
}
|
|
279
|
+
if (!forced) return [];
|
|
280
|
+
const [ref, qty] = forced.split(",")[0].split(":");
|
|
281
|
+
return [{
|
|
282
|
+
kind: "checkout_link",
|
|
283
|
+
index,
|
|
284
|
+
visible: isVisible(element),
|
|
285
|
+
text: label(element),
|
|
286
|
+
package_id: clean(ref) || null,
|
|
287
|
+
quantity: readQuantity(qty),
|
|
288
|
+
next_url: href,
|
|
289
|
+
tag: "a",
|
|
290
|
+
}];
|
|
291
|
+
})
|
|
292
|
+
: [];
|
|
293
|
+
return [...actions, ...links];
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Pick the control to click. A requested --select-package ref must match the
|
|
298
|
+
// control's package id (strict, like selectPackageCard); otherwise the first
|
|
299
|
+
// visible SDK control wins, then the first visible checkout link, and a hidden
|
|
300
|
+
// control is only used when nothing is visible.
|
|
301
|
+
export function chooseCartEntryControl(controls = [], requested = []) {
|
|
302
|
+
const list = Array.isArray(controls) ? controls : [];
|
|
303
|
+
if (!list.length) return { control: null, reason: "no add-to-cart control or forcePackageId checkout link rendered on the entry page" };
|
|
304
|
+
const wanted = Array.isArray(requested) ? requested.filter((item) => item?.packageId) : [];
|
|
305
|
+
if (wanted.length > 1) {
|
|
306
|
+
return {
|
|
307
|
+
control: null,
|
|
308
|
+
reason: `--select-package requested ${wanted.length} refs, but a landing-page entry can select at most one package before the SDK navigates to checkout`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const byRef = wanted.length
|
|
312
|
+
? list.filter((control) => String(control.package_id) === String(wanted[0].packageId))
|
|
313
|
+
: list;
|
|
314
|
+
if (!byRef.length) {
|
|
315
|
+
return {
|
|
316
|
+
control: null,
|
|
317
|
+
reason: `--select-package ${wanted[0].packageId}: no cart-entry control on the entry page carries package ${wanted[0].packageId} (rendered: ${list.map((control) => control.package_id || "(none)").join(", ")})`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// An explicit quantity is a claim about what the click adds. The control
|
|
321
|
+
// states its own quantity (data-next-quantity, or the forcePackageId
|
|
322
|
+
// `ref:qty`), so a request the control cannot satisfy is refused rather
|
|
323
|
+
// than silently downgraded to whatever the control adds.
|
|
324
|
+
const pool = wanted.length && wanted[0].quantityExplicit
|
|
325
|
+
? byRef.filter((control) => Number(control.quantity ?? 1) === Number(wanted[0].quantity))
|
|
326
|
+
: byRef;
|
|
327
|
+
if (!pool.length) {
|
|
328
|
+
return {
|
|
329
|
+
control: null,
|
|
330
|
+
reason: `--select-package ${wanted[0].packageId}:${wanted[0].quantity}: the entry-page control(s) for package ${wanted[0].packageId} add quantity ${[...new Set(byRef.map((control) => control.quantity ?? 1))].join("/")}, not ${wanted[0].quantity}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const byKind = (kind) => pool.filter((control) => control.kind === kind);
|
|
334
|
+
const ordered = [...byKind("add_to_cart"), ...byKind("checkout_link")];
|
|
335
|
+
return { control: ordered.find((control) => control.visible) || ordered[0], reason: null };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// evaluate() body: the SDK cart as the page sees it. Reads the public API
|
|
339
|
+
// first — `window.next.getCartCount()` is installed on every SDK page and
|
|
340
|
+
// returns the store's `totalQuantity` — and the debugger's cart store second
|
|
341
|
+
// (`window.nextDebug.stores.cart`, present with `?debugger=true`), which is
|
|
342
|
+
// the only public place the line items and their package ids are readable.
|
|
343
|
+
// The enriched line list on `getCartData()` is deliberately NOT read: it is
|
|
344
|
+
// always empty on the shipped SDK (campaign-cart#36; see the cart-state
|
|
345
|
+
// verification section of docs/qa-and-test-orders.md), so it would call every
|
|
346
|
+
// cart empty. Reports `readable: false` when neither source is present so the
|
|
347
|
+
// guard can say "could not read" instead of guessing.
|
|
348
|
+
export function sdkCartSnapshotScript() {
|
|
349
|
+
return () => {
|
|
350
|
+
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
351
|
+
const packageIds = (items) => asArray(items).map((item) => item?.packageId ?? item?.package_id ?? null).filter((id) => id !== null && id !== undefined).map(String);
|
|
352
|
+
let debugItems = null;
|
|
353
|
+
try {
|
|
354
|
+
const store = window.nextDebug?.stores?.cart;
|
|
355
|
+
const state = typeof store?.getState === "function" ? store.getState() : store;
|
|
356
|
+
if (state && Array.isArray(state.items)) debugItems = state.items;
|
|
357
|
+
} catch {
|
|
358
|
+
debugItems = null;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const sdk = window.next;
|
|
362
|
+
if (sdk && typeof sdk.getCartCount === "function") {
|
|
363
|
+
const count = Number(sdk.getCartCount());
|
|
364
|
+
return {
|
|
365
|
+
readable: true,
|
|
366
|
+
source: "window.next",
|
|
367
|
+
count: Number.isFinite(count) ? count : 0,
|
|
368
|
+
line_count: debugItems ? debugItems.length : null,
|
|
369
|
+
package_ids: packageIds(debugItems),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// Fall through to the debug store alone.
|
|
374
|
+
}
|
|
375
|
+
if (debugItems) {
|
|
376
|
+
return {
|
|
377
|
+
readable: true,
|
|
378
|
+
source: "nextDebug.stores.cart",
|
|
379
|
+
count: debugItems.reduce((sum, item) => sum + (Number(item?.quantity) || 0), 0),
|
|
380
|
+
line_count: debugItems.length,
|
|
381
|
+
package_ids: packageIds(debugItems),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return { readable: false, source: null, count: null, line_count: null, package_ids: [] };
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// The guard's decision. `cartApi` is the cart-create observation
|
|
389
|
+
// (`cartCreationEvidence`) — the third source the guard may fall back to when
|
|
390
|
+
// the page exposes no SDK global at all.
|
|
391
|
+
export function assessCartBeforeSubmit(snapshot, cartApi = null) {
|
|
392
|
+
if (snapshot?.readable) {
|
|
393
|
+
// Decided on the unit count alone: `totalQuantity` is the store's own
|
|
394
|
+
// number, and the line list is only present under the debugger.
|
|
395
|
+
const empty = Number(snapshot.count) === 0;
|
|
396
|
+
return { empty, source: snapshot.source, count: snapshot.count, line_count: snapshot.line_count ?? null, package_ids: snapshot.package_ids || [] };
|
|
397
|
+
}
|
|
398
|
+
if (cartApi && typeof cartApi.line_count === "number") {
|
|
399
|
+
return { empty: cartApi.line_count === 0, source: "cart_api_response", count: null, line_count: cartApi.line_count, package_ids: [] };
|
|
400
|
+
}
|
|
401
|
+
return { empty: false, source: null, count: null, line_count: null, package_ids: [], unreadable: true };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function cartEmptyMessage(assessment) {
|
|
405
|
+
return `SDK cart holds 0 item(s) at submit time (read from ${assessment.source}); the checkout would never post an order, so the submit click was not made`;
|
|
406
|
+
}
|