Plugin Directory

Changeset 3650174


Ignore:
Timestamp:
08/16/2026 08:31:15 PM (2 months ago)
Author:
shippingcontractguard
Message:

Release Shipping Contract Guard 1.9.0

Location:
shipping-contract-guard
Files:
4 added
16 edited
1 copied

Legend:

Unmodified
Added
Removed
  • shipping-contract-guard/tags/1.9.0/readme.txt

    r3649397 r3650174  
    44Tested up to: 7.0
    55Requires PHP: 8.1
    6 Stable tag: 1.8.1
     6Stable tag: 1.9.0
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1212== Description ==
    1313
    14 Shipping Contract Guard turns important WooCommerce shipping results into repeatable, manager-approved checks.
     14Shipping Contract Guard checks whether the shipping options and prices your store promises still work.
    1515
    16 Start with one shipping promise. For example, if free shipping begins at EUR 75, the plugin checks synthetic carts immediately below, exactly at, and immediately above that boundary. You review the methods and prices WooCommerce returns before saving anything. Later checks compare against that approved version; current behavior is never silently treated as the new baseline.
     16Start with one product, one destination, and one expected shipping result. When the plugin finds a supported rule, it recommends three invented cart totals: just below the boundary, exactly at it, and just above it. You see what WooCommerce returns before you approve the check. Every later run compares the current result with your approved promise, so a changed result cannot silently become normal.
     17
     18The dashboard gives you one clear status: **Everything works**, **Problem found**, or **Could not verify**. When something differs, it explains the shopper impact and the next thing to check. The plugin does this without creating a checkout, customer, or order.
    1719
    1820Local mode is free and requires no account:
    1921
    2022* protect supported WooCommerce core Flat Rate, Free Shipping, and Local Pickup boundaries;
    21 * protect an exact public result returned by another configured shipping method after approving the disclosed quote behavior;
     23* save an exact shipping option and price returned by an enabled method after reviewing how the check will run;
    2224* run invented carts without creating a checkout, customer, or order;
    2325* distinguish **Everything works**, **Problem found**, and **Could not verify**;
    24 * review finite coverage, recent changes, and likely affected approved checks;
    25 * keep 90 days of bounded local history and export JSON, CSV, or a human-readable assurance report; and
    26 * preview and apply one exact core-method availability change with full-contract verification and automatic rollback;
    27 * capture and restore one exact passing core-method state; and
    28 * activate an expiring local continuity path with verified restoration.
     26* review what is covered, recent store changes, and which approved checks may be affected;
     27* keep up to 90 days of local check history and export JSON, CSV, or a readable assurance report;
     28* preview turning one supported WooCommerce shipping option on or off, run every approved check, and roll back automatically if needed;
     29* save one passing setup and restore it later; or
     30* activate a time-limited recovery plan and confirm when the original setup works again.
    2931
    30 Operator is an optional EUR 29/month service for one store. It adds scheduled and post-change checks, independent missed-run supervision, email alerts, minimized offsite evidence, an accountable incident, and verified recovery. A successful checkout page never grants access; only verified billing events and a fresh signed site policy authorize paid work.
     32Operator is an optional EUR 29/month service for one store. It adds scheduled checks, checks after changes, missed-run supervision, email alerts, a named incident owner, and verified recovery. It sends only the limited evidence described below. Returning from checkout does not start paid service. Operator starts only after Stripe confirms the subscription and the plugin receives current service authorization.
    3133
    3234= Deliberate limits =
    3335
    34 Coverage is finite. Unsupported methods, unsafe scenarios, incomplete calculations, and ambiguous evidence remain **Could not verify**. Shipping Contract Guard does not claim that every checkout path is covered or that a preceding configuration change caused a failure.
     36Coverage has limits. Unsupported methods, unsafe scenarios, incomplete calculations, and unclear evidence remain **Could not verify**. Shipping Contract Guard does not claim that every checkout path is covered or that an earlier settings change caused a failure.
    3537
    36 Core suggestions use only public WooCommerce configuration. Provider-agnostic promises use WooCommerce's public returned-rate boundary. The plugin never reads carrier credentials or private provider settings. An explicitly approved public-rate check may cause the configured method to make its ordinary quote request for the invented cart and destination.
     38Core suggestions read only supported WooCommerce settings. For other enabled methods, a saved check uses only the shipping options and prices WooCommerce returns. The plugin never reads carrier credentials or private provider settings. Running an approved check may cause the shipping method to request its normal quote for the invented cart and destination.
    3739
    38 The plugin does not create orders, submit checkout, process payments, buy labels, contact shoppers, or grant remote control over shipping. Its guarded write boundary is limited to the enabled state of one explicitly previewed WooCommerce core Flat Rate, Free Shipping, or Local Pickup zone-method instance. Prices, thresholds, destinations, zone order, products, coupons, provider settings, orders, payments, and labels remain outside that boundary.
     40The plugin does not create orders, submit checkout, process payments, buy labels, contact shoppers, or grant remote control over shipping. After you preview and approve the action, it can turn one supported WooCommerce Flat Rate, Free Shipping, or Local Pickup option on or off. It cannot change prices, thresholds, destinations, zone order, products, coupons, provider settings, orders, payments, or labels.
    3941
    4042== Installation ==
    … …  
    44463. Open **WooCommerce > Shipping Contract Guard**.
    45474. Choose **Preview recommended checks** when a supported rule is detected. Otherwise open **Set up your first protected rule**.
    46 5. Review the generated synthetic carts, expected methods, and prices.
     485. Review the invented carts, expected shipping options, and prices.
    47496. Choose **Save and run first check**.
    4850
    49 The first check should take only a few minutes. Planning, coverage, recovery, and agency tools stay inside the Advanced sections and are not required for initial setup.
     51Planning, coverage, recovery, and agency tools stay inside the Advanced sections. You do not need them for the first check.
    5052
    5153Deployment tooling may run `wp shipping-contract-guard verify`. Exit 0 means safe, 2 means blocked, 3 means the evidence could not decide, and setup or execution errors use exit 1.
    … …  
    5355== Screenshots ==
    5456
    55 1. Protect one detected shipping boundary with a recommended first check.
    56 2. Review every generated synthetic cart, method, and price before approval.
    57 3. See today's plain-language shipping status and one next action.
    58 4. Review bounded check history and detailed expected-versus-observed evidence.
     571. See a named shipping problem, its shopper impact, and the next thing to check.
     582. Review the recommended totals and expected shipping result before saving anything.
     593. Give an Operator incident a clear owner while technical references stay out of the way.
     604. Confirm that the same protected shipping promise passes again after recovery.
    5961== External services ==
    6062
    … …  
    6567= Optional Shipping Contract Guard Operator =
    6668
    67 The plugin makes no Shipping Contract Guard service request until a WooCommerce manager accepts the fixed disclosure. Starting Operator generates an opaque site-bound checkout reference and private signing key locally, then opens Stripe-hosted checkout. The private signing key never goes to Stripe. Verified Stripe subscription and invoice events, not a redirect, establish entitlement.
     69The plugin does not contact the Shipping Contract Guard service until a WooCommerce manager accepts the disclosure shown in the plugin. Starting Operator creates a private signing key and a checkout reference tied to the site, then opens Stripe-hosted checkout. The private signing key never goes to Stripe. Returning from checkout does not start the service; a verified Stripe subscription or invoice event is required.
    6870
    69 After pairing, the plugin contacts `https://api.shippingcontractguard.com` to pair or revoke the site, rotate its public signing identity, retrieve signed policy, configure the selected schedule and alert email, deliver minimized signed run evidence, retrieve current supervision state, and update a manager-submitted incident handoff.
     71After pairing, the plugin contacts `https://api.shippingcontractguard.com` to connect or disconnect the site, refresh its signed authorization, configure the selected schedule and alert email, send the limited check results listed below, retrieve the current monitoring state, and update an incident handoff submitted by a manager.
    7072
    71 Transfers can contain opaque site, contract, run, and case identifiers; a public signing key; timestamps and nonces; configuration fingerprints; pass, fail, unknown, and completeness states; supported boundary kinds and core method classes; hashed method-instance identities; bounded amounts and currency; error codes; and bounded configuration-change facts. A submitted incident can also contain acknowledgement, an operational owner label, and a bounded note. Do not put customer data, credentials, or secrets in those fields.
     73Transfers can contain internal identifiers for the site, saved promise, run, and check; a public signing key; timestamps and one-time request values; cryptographic fingerprints of the shipping configuration; result and completeness states; supported boundary and shipping-method types; hashed method identities; limited cart amounts and currency; error codes; and a limited description of relevant configuration changes. A submitted incident can also contain an acknowledgement, an operational owner label, and a short note. Do not put customer data, credentials, or secrets in those fields.
    7274
    73 The connector excludes products, raw destinations, customers, orders, payment details, merchant payment-processor data, and carrier credentials. The service retains evidence according to the subscribed plan, initially 90 days. Disconnecting stops new paid work and transfer. An organization owner can export data or request deletion; deletion remains cancelable for 24 hours before tenant records are purged, apart from a minimal replay-protection tombstone.
     75The connector excludes products, full destinations, customers, orders, payment details, merchant payment-processor data, and carrier credentials. The service retains evidence according to the subscribed plan, initially 90 days. Disconnecting stops new paid work and data transfer. An organization owner can export data or request deletion. A deletion request can be canceled for 24 hours; afterward, the service removes the organization's records but keeps a minimal record that prevents an old request from being reused.
    7476
    7577Privacy policy: https://shippingcontractguard.com/privacy.html
    … …  
    8183== Privacy ==
    8284
    83 Local data includes approved contract definitions, synthetic case results, configuration fingerprints and bounded change facts, run timestamps, local operational decisions, and optional service state. Completed local runs are retained for up to 90 days and 500 records. Deactivation preserves data. Uninstall preserves data unless an administrator explicitly enables delete-on-uninstall.
     85Local data includes approved shipping promises, invented-cart results, cryptographic fingerprints and limited facts about settings changes, run times, local incident decisions, and optional service state. Completed local runs are retained for up to 90 days and 500 records. Deactivation preserves data. Uninstall preserves data unless an administrator explicitly enables delete-on-uninstall.
    8486
    8587The plugin adds suggested text to **Settings > Privacy**. Use invented test products and destinations; never enter customer data, credentials, or secrets. A privacy-safe diagnostic download contains only versions, schema and scheduler state, connection presence, and aggregate latest-run counts. Optional WooCommerce logging is off by default and records only an admin action name plus success or error outcome.
    … …  
    8991= Does local verification require an account? =
    9092
    91 No. Contract setup, approval, manual checks, local history, diagnostics, and exports work without an account or Shipping Contract Guard service request.
     93No. Creating and approving checks, running them manually, viewing local history, using diagnostics, and exporting results all work without an account or Shipping Contract Guard service request.
    9294
    93 = Does this inspect my shipping plugin? =
     95= Does this read private carrier settings? =
    9496
    95 No. Core suggestions support WooCommerce core methods. A provider-agnostic promise records only method identities and prices returned through WooCommerce's public shipping calculation after explicit approval.
     97No. Core suggestions use WooCommerce core settings. A public-result promise records only the shipping method identities and prices returned by WooCommerce after you approve the check.
    9698
    9799= Does this test checkout or payment? =
    98100
    99 No. It calculates isolated synthetic shipping packages without submitting checkout or creating an order. Stripe is contacted only when a manager chooses the optional Operator subscription.
     101No. It asks WooCommerce for shipping results for an invented cart. It does not open or submit checkout, and it creates no order. Stripe is contacted only when a manager chooses the optional Operator subscription.
    100102
    101103= What does Could not verify mean? =
    … …  
    109111= Are Cart and Checkout blocks supported? =
    110112
    111 Current block checkout is included in the finite browser matrix. WooCommerce 8.2 block rendering is excluded because that version creates a checkout-draft order merely by rendering, which violates the plugin's zero-order test invariant. Classic checkout remains covered at the minimum boundary.
     113Yes. Current Cart and Checkout blocks are covered by our browser tests. For WooCommerce 8.2, we test classic checkout only because merely opening its block checkout creates a draft order; our tests must never create orders.
    112114
    113115== Changelog ==
     116
     117= 1.9.0 =
     118
     119* Makes the first protected promise a clear recommendation with one product, one location, and three boundary totals.
     120* Keeps custom setup available under a plain “Customize” control.
     121* Leads each result with the protected promise, its scope, the consequence, and one next check.
     122* Adds test coverage for approved shipping results returned by WooCommerce.
    114123
    115124= 1.8.1 =
    … …  
    122131* Simplified the primary manager navigation and moved planning, coverage, and guarded-change workflows behind Advanced sections.
    123132* Added privacy-safe Site Health information, a diagnostic download, opt-in bounded WooCommerce logging, and direct documentation/support links.
    124 * Added an exact core-method change preview, passing-evidence known-good vault, and bounded continuity playbook.
    125 * Kept guarded core-method application and continuity local and free. Operator remains the optional external supervision service; no price, threshold, zone-order, opaque-provider, order, payment, or remote-command authority was added.
     133* Added a reviewed on/off change for one supported WooCommerce shipping option, a saved passing setup, and a time-limited recovery plan.
     134* Kept these recovery tools local and free. Operator remains optional and cannot change prices, thresholds, destinations, zone order, provider settings, orders, payments, or labels.
    126135
    127136= 1.7.2 =
  • shipping-contract-guard/tags/1.9.0/shipping-contract-guard.php

    r3649397 r3650174  
    33 * Plugin Name:       Shipping Contract Guard for WooCommerce
    44 * Description:       Catch broken WooCommerce shipping rules before customers do.
    5  * Version:           1.8.1
     5 * Version:           1.9.0
    66 * Requires at least: 6.5
    77 * Requires PHP:      8.1
    … …  
    2222defined( 'ABSPATH' ) || exit;
    2323
    24 define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1.8.1' );
     24define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1.9.0' );
    2525define( 'SHIPPING_CONTRACT_GUARD_FILE', __FILE__ );
    2626define( 'SHIPPING_CONTRACT_GUARD_DIR', plugin_dir_path( __FILE__ ) );
  • shipping-contract-guard/tags/1.9.0/src/Admin/Actions.php

    r3649397 r3650174  
    945945                        $criticality       = isset( $request['criticality'] ) ? sanitize_key( $request['criticality'] ) : 'release_blocking';
    946946                        if ( 1 !== preg_match( '/^[A-Z]{2}$/', $country ) || $product < 1 || strlen( $postcode_positive ) > 20 || strlen( $postcode_negative ) > 20 || strlen( $coupon ) > 100 || '' === $rule_name || strlen( $rule_name ) > 100 || '' === $group_name || strlen( $group_name ) > 80 || ! in_array( $criticality, array( 'advisory', 'standard', 'release_blocking' ), true ) || 1 !== preg_match( '/^[a-f0-9]{64}$/', $candidate ) ) {
    947                                 throw new \InvalidArgumentException( 'Country, synthetic product, coupon, or configured threshold selection is invalid.' );
     947                                throw new \InvalidArgumentException( 'The country, test product, coupon, or selected Free Shipping rule is invalid.' );
    948948                        }
    949949                        $primary = $this->shippable_product( $product );
  • shipping-contract-guard/tags/1.9.0/src/Admin/Assets.php

    r3649397 r3650174  
    1818                wp_add_inline_style(
    1919                        'shipping-contract-guard-admin',
    20                         '.shipping-contract-guard-wrap{max-width:1080px}.scg-page-header{padding:24px 0 10px}.scg-page-header h1{font-size:34px;letter-spacing:-.02em}.scg-eyebrow{margin:0 0 4px;color:#3858a7;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.scg-lead,.scg-section-lead{max-width:760px;font-size:17px;color:#50575e}.scg-section-lead{margin-top:-4px}.scg-trust-list{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 0;padding:0;list-style:none}.scg-trust-list li{padding:5px 10px;border:1px solid #c3c4c7;border-radius:999px;background:#fff;color:#3c434a;font-size:12px;font-weight:600}.scg-status-card,.scg-panel,.scg-preview,.scg-upgrade{margin:20px 0;padding:24px;background:#fff;border:1px solid #dcdcde;border-radius:10px;box-shadow:0 1px 2px rgba(0,0,0,.03)}.scg-status-card{border-left-width:5px}.scg-status-setup,.scg-status-ready,.scg-status-running,.scg-status-unknown{border-left-color:#dba617}.scg-status-pass{border-left-color:#008a20}.scg-status-fail{border-left-color:#d63638}.scg-status-card h2,.scg-panel>h2{margin-top:4px}.scg-dashboard-facts{display:flex;gap:12px;flex-wrap:wrap;margin:18px 0;padding:0;list-style:none}.scg-dashboard-facts li{min-width:150px;padding:12px 14px;border-radius:8px;background:#f6f7f7}.scg-dashboard-facts strong{display:block;font-size:18px}.scg-tabs{margin:24px 0 0}.scg-example{margin:16px 0;padding:14px;background:#f0f6fc;border-left:4px solid #2271b1}.scg-recommendation{margin:18px 0;padding:18px;border:1px solid #72aee6;border-radius:8px;background:#f0f6fc}.scg-recommendation h3{margin:4px 0}.scg-coverage-group>.scg-panel:first-child{margin-top:20px}.scg-advanced-tools{margin:20px 0;padding:16px 20px;border:1px solid #c3c4c7;border-radius:10px;background:#f6f7f7}.scg-advanced-tools>summary{display:flex;min-height:24px;align-items:center;cursor:pointer;font-size:16px;font-weight:600}.scg-setup-form{max-width:780px}.scg-step{display:grid;grid-template-columns:38px 1fr;gap:14px;margin:24px 0}.scg-step-number{display:flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:50%;background:#2271b1;color:#fff;font-weight:700}.scg-step label,.scg-step legend{display:block;margin-bottom:6px}.scg-step select{width:100%;max-width:620px;color:#1d2327!important;background:#fff!important}.scg-step input[type=text],.scg-step input:not([type]){max-width:300px}.scg-step fieldset{border:0;padding:0}.scg-step fieldset label{margin-top:12px}.scg-preview{border-color:#8c8f94}.scg-preview table{margin:14px 0}.scg-technical-definition{max-height:24rem;overflow:auto;white-space:pre-wrap}.scg-result{display:inline-block;padding:4px 9px;border-radius:999px;font-weight:600}.scg-result-pass{background:#edfaef;color:#006b1b}.scg-result-fail{background:#fcf0f1;color:#8a2424}.scg-result-unknown,.scg-result-running{background:#fcf5e8;color:#6f4f00}.scg-history td,.scg-run-report td{vertical-align:middle}.scg-panel details{margin:16px 0}.scg-panel summary{cursor:pointer;font-weight:600}.scg-run-report{padding:16px;border:1px solid #c3c4c7;border-radius:8px}.scg-run-report table{margin-top:14px}.scg-change-report{margin-top:18px;padding:12px 16px;background:#f6f7f7}.scg-upgrade{background:#f0f6fc;border-color:#72aee6}.scg-twin{border-color:#72aee6;background:#f8fbff;color:#1d2327}.scg-twin-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:22px}.scg-twin-card{padding:18px;border:1px solid #dcdcde;border-radius:9px;background:#fff}.scg-twin-card h3{margin:4px 0 8px}.scg-twin-card label{display:block;margin:10px 0 4px;font-weight:600}.scg-twin-card select,.scg-twin-card input[type=number]{width:100%;max-width:430px;color:#1d2327!important;background:#fff!important}.scg-twin-card .button{margin-top:12px}.scg-twin-budget{grid-column:1/-1}.scg-twin-list{margin:12px 0 0}.scg-twin-list li{margin:8px 0}.scg-twin-list span{display:block;color:#50575e;font-size:12px}.scg-twin-result{margin-top:14px;padding:12px;border-left:4px solid #3858a7;background:#f0f6fc}.scg-recovery{border-left:5px solid #3858a7}.scg-recovery-controls{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:22px 0}.scg-recovery-controls>div,.scg-recovery-result{padding:18px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-recovery-controls label,.scg-recovery-preview label{display:block;margin:10px 0 4px;font-weight:600}.scg-recovery-controls select,.scg-recovery-preview textarea{width:100%;max-width:520px}.scg-recovery-controls select,.scg-review-queue select{color:#1d2327!important;background:#fff!important}.scg-review-queue{margin:18px 0}.scg-review-queue>li{margin:14px 0;padding:16px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-review-queue label{display:block;margin:10px 0 4px;font-weight:600}.scg-review-queue select,.scg-review-queue textarea{width:100%;max-width:520px}.scg-recovery-preview form{margin-top:16px}.scg-digest{overflow-wrap:anywhere}.scg-inline-fields{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.scg-inline-fields input{display:block;max-width:130px}.scg-operator-facts,.scg-operator-compare{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}.scg-operator-facts>div,.scg-operator-compare>div{padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#fff}.scg-operator-facts strong,.scg-operator-facts span,.scg-operator-compare strong,.scg-operator-compare span{display:block}.scg-operator-facts strong{overflow-wrap:anywhere;font-size:15px}.scg-operator-facts span,.scg-operator-compare span{margin-top:4px;color:#50575e}.scg-operator-compare{grid-template-columns:1fr 1fr}.scg-operator-compare>div:last-child{border-color:#72aee6}.scg-value-list li{margin:8px 0}.scg-operator-gate{padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-activation-checklist{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:18px 0;padding:0;list-style:none;counter-reset:activation}.scg-activation-checklist li{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:start;padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7;counter-increment:activation}.scg-activation-checklist li>span:last-child{display:grid;gap:3px;color:#50575e}.scg-activation-checklist strong{color:#1d2327}.scg-operator-incidents{margin:18px 0;padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-operator-settings{margin-top:22px;padding-top:18px;border-top:1px solid #dcdcde}@media(max-width:782px){.scg-step{grid-template-columns:1fr}.scg-step-number{margin-bottom:4px}.scg-history,.scg-run-report table{display:block;overflow-x:auto}.scg-dashboard-facts,.scg-trust-list{display:block}.scg-dashboard-facts li,.scg-trust-list li{display:table;margin:6px 0}.scg-twin-grid,.scg-recovery-controls,.scg-activation-checklist{grid-template-columns:1fr}.scg-twin-budget{grid-column:auto}.scg-operator-facts,.scg-operator-compare{grid-template-columns:1fr}}'
     20                        '.shipping-contract-guard-wrap{max-width:1080px}.scg-page-header{padding:24px 0 10px}.scg-page-header h1{font-size:34px;letter-spacing:-.02em}.scg-eyebrow{margin:0 0 4px;color:#3858a7;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.scg-lead,.scg-section-lead{max-width:760px;font-size:17px;color:#50575e}.scg-section-lead{margin-top:-4px}.scg-trust-list{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 0;padding:0;list-style:none}.scg-trust-list li{padding:5px 10px;border:1px solid #c3c4c7;border-radius:999px;background:#fff;color:#3c434a;font-size:12px;font-weight:600}.scg-status-card,.scg-panel,.scg-preview,.scg-upgrade{margin:20px 0;padding:24px;background:#fff;border:1px solid #dcdcde;border-radius:10px;box-shadow:0 1px 2px rgba(0,0,0,.03)}.scg-status-card{border-left-width:5px}.scg-status-setup,.scg-status-ready,.scg-status-running,.scg-status-unknown{border-left-color:#dba617}.scg-status-pass{border-left-color:#008a20}.scg-status-fail{border-left-color:#d63638}.scg-status-card h2,.scg-panel>h2{margin-top:4px}.scg-dashboard-facts{display:flex;gap:12px;flex-wrap:wrap;margin:18px 0;padding:0;list-style:none}.scg-dashboard-facts li{min-width:150px;padding:12px 14px;border-radius:8px;background:#f6f7f7}.scg-dashboard-facts strong{display:block;font-size:18px}.scg-tabs{margin:24px 0 0}.scg-example{margin:16px 0;padding:14px;background:#f0f6fc;border-left:4px solid #2271b1}.scg-recommendation{margin:18px 0;padding:18px;border:1px solid #72aee6;border-radius:8px;background:#f0f6fc}.scg-recommendation h3{margin:4px 0}.scg-boundary-summary{display:grid;grid-template-columns:repeat(3,minmax(110px,1fr));gap:10px;max-width:620px;margin:16px 0;padding:0;list-style:none}.scg-boundary-summary li{padding:10px 12px;border:1px solid #c3c4c7;border-radius:7px;background:#fff}.scg-boundary-summary span,.scg-boundary-summary strong{display:block}.scg-boundary-summary span{color:#50575e;font-size:12px}.scg-preview-summary{margin:14px 0;padding:14px 18px 14px 34px;background:#f6f7f7}.scg-preview-summary li{margin:5px 0}.scg-promise-verdict{margin:16px 0;padding:16px;border-left:4px solid #3858a7;background:#f0f6fc}.scg-promise-verdict h3{margin-top:0}.scg-coverage-group>.scg-panel:first-child{margin-top:20px}.scg-advanced-tools{margin:20px 0;padding:16px 20px;border:1px solid #c3c4c7;border-radius:10px;background:#f6f7f7}.scg-advanced-tools>summary{display:flex;min-height:24px;align-items:center;cursor:pointer;font-size:16px;font-weight:600}.scg-setup-form{max-width:780px}.scg-step{display:grid;grid-template-columns:38px 1fr;gap:14px;margin:24px 0}.scg-step-number{display:flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:50%;background:#2271b1;color:#fff;font-weight:700}.scg-step label,.scg-step legend{display:block;margin-bottom:6px}.scg-step select{width:100%;max-width:620px;color:#1d2327!important;background:#fff!important}.scg-step input[type=text],.scg-step input:not([type]){max-width:300px}.scg-step fieldset{border:0;padding:0}.scg-step fieldset label{margin-top:12px}.scg-preview{border-color:#8c8f94}.scg-preview table{margin:14px 0}.scg-technical-definition{max-height:24rem;overflow:auto;white-space:pre-wrap}.scg-result{display:inline-block;padding:4px 9px;border-radius:999px;font-weight:600}.scg-result-pass{background:#edfaef;color:#006b1b}.scg-result-fail{background:#fcf0f1;color:#8a2424}.scg-result-unknown,.scg-result-running{background:#fcf5e8;color:#6f4f00}.scg-history td,.scg-run-report td{vertical-align:middle}.scg-panel details{margin:16px 0}.scg-panel summary{cursor:pointer;font-weight:600}.scg-run-report{padding:16px;border:1px solid #c3c4c7;border-radius:8px}.scg-run-report table{margin-top:14px}.scg-change-report{margin-top:18px;padding:12px 16px;background:#f6f7f7}.scg-upgrade{background:#f0f6fc;border-color:#72aee6}.scg-twin{border-color:#72aee6;background:#f8fbff;color:#1d2327}.scg-twin-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:22px}.scg-twin-card{padding:18px;border:1px solid #dcdcde;border-radius:9px;background:#fff}.scg-twin-card h3{margin:4px 0 8px}.scg-twin-card label{display:block;margin:10px 0 4px;font-weight:600}.scg-twin-card select,.scg-twin-card input[type=number]{width:100%;max-width:430px;color:#1d2327!important;background:#fff!important}.scg-twin-card .button{margin-top:12px}.scg-twin-budget{grid-column:1/-1}.scg-twin-list{margin:12px 0 0}.scg-twin-list li{margin:8px 0}.scg-twin-list span{display:block;color:#50575e;font-size:12px}.scg-twin-result{margin-top:14px;padding:12px;border-left:4px solid #3858a7;background:#f0f6fc}.scg-recovery{border-left:5px solid #3858a7}.scg-recovery-controls{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:22px 0}.scg-recovery-controls>div,.scg-recovery-result{padding:18px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-recovery-controls label,.scg-recovery-preview label{display:block;margin:10px 0 4px;font-weight:600}.scg-recovery-controls select,.scg-recovery-preview textarea{width:100%;max-width:520px}.scg-recovery-controls select,.scg-review-queue select{color:#1d2327!important;background:#fff!important}.scg-review-queue{margin:18px 0}.scg-review-queue>li{margin:14px 0;padding:16px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-review-queue label{display:block;margin:10px 0 4px;font-weight:600}.scg-review-queue select,.scg-review-queue textarea{width:100%;max-width:520px}.scg-recovery-preview form{margin-top:16px}.scg-digest{overflow-wrap:anywhere}.scg-inline-fields{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.scg-inline-fields input{display:block;max-width:130px}.scg-operator-facts,.scg-operator-compare{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}.scg-operator-facts>div,.scg-operator-compare>div{padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#fff}.scg-operator-facts strong,.scg-operator-facts span,.scg-operator-compare strong,.scg-operator-compare span{display:block}.scg-operator-facts strong{overflow-wrap:break-word;word-break:normal;font-size:15px}.scg-operator-facts span,.scg-operator-compare span{margin-top:4px;color:#50575e}.scg-operator-compare{grid-template-columns:1fr 1fr}.scg-operator-compare>div:last-child{border-color:#72aee6}.scg-value-list li{margin:8px 0}.scg-operator-gate{padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-activation-checklist{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:18px 0;padding:0;list-style:none;counter-reset:activation}.scg-activation-checklist li{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:start;padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7;counter-increment:activation}.scg-activation-checklist li>span:last-child{display:grid;gap:3px;color:#50575e}.scg-activation-checklist strong{color:#1d2327}.scg-operator-incidents{margin:18px 0;padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-technical-reference{font-size:12px}.scg-operator-settings{margin-top:22px;padding-top:18px;border-top:1px solid #dcdcde}@media(max-width:1200px){.scg-operator-facts{grid-template-columns:repeat(2,1fr)}}@media(max-width:782px){.scg-step{grid-template-columns:1fr}.scg-step-number{margin-bottom:4px}.scg-history,.scg-run-report table{display:block;overflow-x:auto}.scg-dashboard-facts,.scg-trust-list{display:block}.scg-dashboard-facts li,.scg-trust-list li{display:table;margin:6px 0}.scg-boundary-summary{grid-template-columns:1fr}.scg-twin-grid,.scg-recovery-controls,.scg-activation-checklist{grid-template-columns:1fr}.scg-twin-budget{grid-column:auto}.scg-operator-facts,.scg-operator-compare{grid-template-columns:1fr}}'
    2121                );
    2222                wp_add_inline_style(
  • shipping-contract-guard/tags/1.9.0/src/Admin/Page.php

    r3649397 r3650174  
    33namespace ShippingContractGuard\Admin;
    44
     5use ShippingContractGuard\Application\FirstPromiseRecommendation;
     6use ShippingContractGuard\Application\PromiseVerdictPresentation;
    57use ShippingContractGuard\LocalApplication;
    68use ShippingContractGuard\Lifecycle\ConfigurationChangeAutomation;
    … …  
    219221                $notice = get_transient( 'shipping_contract_guard_notice_' . get_current_user_id() );
    220222                delete_transient( 'shipping_contract_guard_notice_' . get_current_user_id() );
     223                $recommendation   = ( new FirstPromiseRecommendation() )->recommend(
     224                        $candidates,
     225                        array_map(
     226                                static fn( $product ): array => array(
     227                                        'id'   => (int) $product->get_id(),
     228                                        'name' => trim( (string) $product->get_name() ),
     229                                ),
     230                                $products
     231                        )
     232                );
     233                $activeDefinition = is_array( $active ) ? json_decode( (string) ( $active['definition_json'] ?? '' ), true ) : null;
     234                if ( is_array( $report ) && (int) ( $report['run']['contract_row_id'] ?? 0 ) !== (int) ( $active['id'] ?? 0 ) ) {
     235                        $activeDefinition = null;
     236                }
     237                $verdictPresentation = ( new PromiseVerdictPresentation() )->present(
     238                        is_array( $latest ) ? $latest : null,
     239                        is_array( $activeDefinition ) ? $activeDefinition : null,
     240                        is_array( $report['cases'] ?? null ) ? $report['cases'] : array(),
     241                        is_array( $changeReadiness ) && 'verification_due' === ( $changeReadiness['state'] ?? null )
     242                );
    221243                ?>
    222244                <div class="wrap shipping-contract-guard-wrap">
    … …  
    240262                        </nav>
    241263
    242                         <?php $this->dashboard_panel( $active, $latest, count( $rules ), $changeReadiness, $coverage, $daily_pulse, $incidents ); ?>
     264                        <?php $this->dashboard_panel( $active, $latest, count( $rules ), $changeReadiness, $coverage, $daily_pulse, $incidents, $verdictPresentation ); ?>
    243265
    244266                        <section id="scg-promises" class="scg-panel" aria-labelledby="scg-promises-heading">
    245267                                <h2 id="scg-promises-heading"><?php echo esc_html( is_array( $active ) ? __( 'Your protected shipping promises', 'shipping-contract-guard' ) : __( 'Protect your first shipping promise', 'shipping-contract-guard' ) ); ?></h2>
    246268                                <p><?php echo esc_html( is_array( $active ) ? __( 'These saved expectations are checked together. Open the authoring controls only when you need to add or replace a protected rule.', 'shipping-contract-guard' ) : __( 'Choose one of your existing free-shipping rules. We will check the cart total immediately below, exactly at, and immediately above its threshold.', 'shipping-contract-guard' ) ); ?></p>
    247                                 <?php $this->recommended_setup( $active, $candidates, $products ); ?>
     269                                <?php $this->recommended_setup( $active, $recommendation, $candidates ); ?>
    248270                                <?php if ( $rules ) : ?>
    249271                                        <div class="scg-protected-rules"><h3><?php echo esc_html__( 'Protected rules', 'shipping-contract-guard' ); ?></h3><ul>
    … …  
    256278                                <?php endif; ?>
    257279
    258                                 <details class="scg-advanced-tools scg-promise-authoring" <?php echo is_array( $active ) ? '' : 'open'; ?>>
    259                                         <summary><?php echo esc_html( is_array( $active ) ? __( 'Add or replace a protected rule', 'shipping-contract-guard' ) : __( 'Set up your first protected rule', 'shipping-contract-guard' ) ); ?></summary>
     280                                <details class="scg-advanced-tools scg-promise-authoring">
     281                                        <summary><?php echo esc_html__( 'Customize', 'shipping-contract-guard' ); ?></summary>
    260282                                        <div class="scg-example" role="note">
    261283                                                <strong><?php echo esc_html__( 'Example:', 'shipping-contract-guard' ); ?></strong>
    … …  
    329351                                        <?php endif; ?>
    330352                                </form>
    331                                 </details>
    332353
    333354                                <div class="scg-authored-promise">
    334355                                        <h3><?php echo esc_html__( 'Protect any WooCommerce shipping result', 'shipping-contract-guard' ); ?></h3>
    335356                                        <p><?php echo esc_html__( 'No supported threshold? Define one exact synthetic cart and destination. Shipping Contract Guard records the method identities and prices returned by WooCommerce, then asks you to approve them as the promise to protect.', 'shipping-contract-guard' ); ?></p>
    336                                         <details <?php echo ! $candidates ? 'open' : ''; ?>>
     357                                        <details>
    337358                                                <summary><?php echo esc_html__( 'Create a provider-agnostic shipping promise', 'shipping-contract-guard' ); ?></summary>
    338359                                                <form class="scg-setup-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    … …  
    395416                                        </details>
    396417                                </div>
     418                                </details>
    397419
    398420                                <?php $this->draft_previews( $contracts ); ?>
    … …  
    436458                                </tbody></table>
    437459                                <p class="description"><?php echo esc_html__( 'Assurance report downloads a human-readable, unsigned summary for review before sharing. It sends nothing. Problem found means WooCommerce differed from your saved expectations. Could not verify means an observation was unsafe, ambiguous, or incomplete; it is never counted as working.', 'shipping-contract-guard' ); ?></p>
    438                                 <?php $this->latest_report( $report ); ?>
     460                                <?php $this->latest_report( $report, $verdictPresentation ); ?>
    439461                        </section>
    440462
    … …  
    467489                        </details>
    468490
    469                         <?php $this->operator_panel( $rules, $runs, $connection, $policy, $service_disclosure, $checkout_intent, $alert_email, $alert_test, $incidents, $maintenance_windows, $activation_receipt ); ?>
     491                        <?php $this->operator_panel( $active, $rules, $runs, $connection, $policy, $service_disclosure, $checkout_intent, $alert_email, $alert_test, $incidents, $maintenance_windows, $activation_receipt ); ?>
    470492
    471493                        <section id="scg-settings" class="scg-panel" aria-labelledby="scg-settings-heading">
    … …  
    632654                        <p class="scg-eyebrow"><?php echo esc_html__( 'Guard before, preserve, and recover', 'shipping-contract-guard' ); ?></p>
    633655                        <h2 id="scg-recovery-heading"><?php echo esc_html__( 'Guarded shipping changes', 'shipping-contract-guard' ); ?></h2>
    634                         <p class="scg-section-lead"><?php echo esc_html__( 'Preview the approved-promise impact of one exact core method change, preserve a passing known-good state, or prepare an expiring continuity path.', 'shipping-contract-guard' ); ?></p>
     656                        <p class="scg-section-lead"><?php echo esc_html__( 'Preview turning one supported WooCommerce shipping option on or off, save a passing setup, or prepare a time-limited recovery plan.', 'shipping-contract-guard' ); ?></p>
    635657                        <p><?php echo esc_html__( 'This cannot edit prices, thresholds, destinations, products, coupons, carrier settings, orders, payments, or deployments. The action is recorded before mutation and rolls back on a blocked, incomplete, or errored verification.', 'shipping-contract-guard' ); ?></p>
    636658
    … …  
    20322054        }
    20332055
    2034         private function latest_report( $report ) {
     2056        private function latest_report( $report, array $presentation ) {
    20352057                if ( ! is_array( $report ) || ! is_array( $report['run'] ?? null ) || ! is_array( $report['cases'] ?? null ) ) {
    20362058                        return;
    20372059                }
    2038                 $run  = $report['run'];
    2039                 $view = $this->outcome_view( $run );
    2040                 $open = 'pass' !== ( $run['outcome'] ?? '' );
     2060                $run      = $report['run'];
     2061                $view     = $this->outcome_view( $run );
     2062                $headline = $this->verdict_headline( $presentation, $view['headline'] );
     2063                $open     = 'pass' !== ( $presentation['state'] ?? '' );
    20412064                ?>
    20422065                <details class="scg-run-report" <?php echo $open ? 'open' : ''; ?>>
    … …  
    20452068                        echo esc_html(
    20462069                                sprintf(
    2047                                 /* translators: %s: latest shipping-check outcome. */
     2070                                        /* translators: %s: scoped latest shipping-check headline. */
    20482071                                        __( 'Latest check details — %s', 'shipping-contract-guard' ),
    2049                                         $view['label']
     2072                                        $headline
    20502073                                )
    20512074                        );
    20522075                        ?>
    20532076                        </summary>
     2077                        <div class="scg-promise-verdict">
     2078                                <h3><?php echo esc_html( $headline ); ?></h3>
     2079                                <p><?php echo esc_html( $this->verdict_consequence( $presentation, $run ) ); ?></p>
     2080                                <?php $scope = $this->verdict_scope( (array) ( $presentation['scope'] ?? array() ) ); ?>
     2081                                <?php
     2082                                if ( '' !== $scope ) :
     2083                                        ?>
     2084                                        <p><strong><?php echo esc_html__( 'Saved example:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $scope ); ?></p><?php endif; ?>
     2085                                <?php
     2086                                if ( is_string( $presentation['next_check'] ?? null ) ) :
     2087                                        ?>
     2088                                        <p class="scg-next-action"><strong><?php echo esc_html__( 'Start here:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $this->diagnosis_label( (string) $presentation['next_check'] ) ); ?></p><?php endif; ?>
     2089                                <?php
     2090                                if ( true === ( $presentation['limits_causal'] ?? false ) ) :
     2091                                        ?>
     2092                                        <p class="description"><?php echo esc_html__( 'This suggestion narrows the investigation. It does not prove what caused the result and it does not change store settings.', 'shipping-contract-guard' ); ?></p><?php endif; ?>
     2093                        </div>
    20542094                        <p><?php echo esc_html__( 'Compare what the saved shipping rule expected with what WooCommerce returned. No checkout, customer, or order was created.', 'shipping-contract-guard' ); ?></p>
    20552095                        <table class="widefat striped"><thead><tr><th scope="col"><?php echo esc_html__( 'Result', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Test case', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Expected', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Observed', 'shipping-contract-guard' ); ?></th></tr></thead><tbody>
    … …  
    21132153
    21142154        private function report_case_context( array $case ) {
    2115                 $context     = is_array( $case['local_context'] ?? null ) ? $case['local_context'] : array();
    2116                 $currency    = (string) ( $context['currency'] ?? '' );
    2117                 $amount      = '' !== $currency
     2155                $context    = is_array( $case['local_context'] ?? null ) ? $case['local_context'] : array();
     2156                $has_amount = array_key_exists( 'amount_minor', $context );
     2157                $currency   = trim( (string) ( $context['currency'] ?? '' ) );
     2158                if ( $has_amount && '' === $currency ) {
     2159                        $currency = (string) get_woocommerce_currency();
     2160                }
     2161                $amount      = $has_amount && '' !== $currency
    21182162                        ? wc_price( (int) ( $context['amount_minor'] ?? 0 ) / ( 10 ** wc_get_price_decimals() ), array( 'currency' => $currency ) )
    21192163                        : esc_html__( 'Unknown cart total', 'shipping-contract-guard' );
    … …  
    21322176
    21332177        private function report_expected_methods( array $case ) {
    2134                 $contract = is_array( $case['expected_contract'] ?? null ) ? $case['expected_contract'] : array();
    2135                 $labels   = array();
     2178                $contract        = is_array( $case['expected_contract'] ?? null ) ? $case['expected_contract'] : array();
     2179                $requiredLabels  = array();
     2180                $requiredMethods = array();
     2181                $absentLabels    = array();
    21362182                foreach ( (array) ( $contract['method_expectations'] ?? array() ) as $expectation ) {
    21372183                        if ( ! is_array( $expectation ) ) {
    … …  
    21402186                        $label = $this->method_label( (string) ( $expectation['method_id'] ?? '' ) );
    21412187                        if ( true !== ( $expectation['expected'] ?? false ) ) {
    2142                                 $labels[] = sprintf(
     2188                                $absentLabels[ $label ] = true;
     2189                                continue;
     2190                        }
     2191                        $minimum                   = is_array( $expectation['minimum'] ?? null ) ? $expectation['minimum'] : null;
     2192                        $maximum                   = is_array( $expectation['maximum'] ?? null ) ? $expectation['maximum'] : null;
     2193                        $requiredMethods[ $label ] = true;
     2194                        $label                    .= $this->price_contract_label( $minimum, $maximum, is_array( $expectation['relative'] ?? null ) ? $expectation['relative'] : null );
     2195                        $requiredLabels[]          = $label;
     2196                }
     2197                if ( ! $requiredLabels ) {
     2198                        $result = __( 'No shipping method must be offered', 'shipping-contract-guard' );
     2199                } else {
     2200                        $absent = array_map(
     2201                                static fn( string $label ): string => sprintf(
    21432202                                        /* translators: %s: shipping-method label which must not be available. */
    21442203                                        __( 'No %s', 'shipping-contract-guard' ),
    21452204                                        $label
    2146                                 );
    2147                                 continue;
    2148                         }
    2149                         $minimum  = is_array( $expectation['minimum'] ?? null ) ? $expectation['minimum'] : null;
    2150                         $maximum  = is_array( $expectation['maximum'] ?? null ) ? $expectation['maximum'] : null;
    2151                         $label   .= $this->price_contract_label( $minimum, $maximum, is_array( $expectation['relative'] ?? null ) ? $expectation['relative'] : null );
    2152                         $labels[] = $label;
    2153                 }
    2154                 $result  = $labels ? implode( ', ', $labels ) : __( 'No shipping method must be offered', 'shipping-contract-guard' );
     2205                                ),
     2206                                array_keys( array_diff_key( $absentLabels, $requiredMethods ) )
     2207                        );
     2208                        $result = implode( ', ', array_merge( $requiredLabels, $absent ) );
     2209                }
    21552210                $quality = is_array( $contract['quality'] ?? null ) ? $contract['quality'] : null;
    21562211                if ( is_array( $quality ) ) {
    … …  
    22812336        }
    22822337
    2283         private function dashboard_panel( $active, $latest, $rule_count, $readiness, $coverage, $dailyPulse, array $incidents ) {
     2338        private function dashboard_panel( $active, $latest, $rule_count, $readiness, $coverage, $dailyPulse, array $incidents, array $presentation ) {
     2339                $latestState     = is_array( $latest ) ? (string) ( $presentation['state'] ?? $latest['outcome'] ?? '' ) : '';
     2340                $latestAttention = in_array( $latestState, array( 'fail', 'unknown' ), true );
    22842341                if ( ! is_array( $active ) ) {
    22852342                        $key   = 'setup';
    … …  
    22952352                        );
    22962353                } else {
    2297                         $view     = $this->outcome_view( $latest );
    2298                         $key      = $view['key'];
    2299                         $title    = $view['headline'];
    2300                                 $text = sprintf(
     2354                        $view  = $this->outcome_view( $latest );
     2355                        $key   = 'stale' === ( $presentation['state'] ?? '' ) ? 'unknown' : $view['key'];
     2356                        $title = $this->verdict_headline( $presentation, $view['headline'] );
     2357                        $text  = true === ( $presentation['named'] ?? false ) ? $this->verdict_consequence( $presentation, $latest ) : sprintf(
    23012358                                        /* translators: 1: UTC check timestamp, 2: passing count, 3: failing count, 4: incomplete count. */
    2302                                         __( 'Last checked %1$s UTC: %2$d working, %3$d problems, %4$d could not be verified.', 'shipping-contract-guard' ),
    2303                                         (string) ( $latest['started_at'] ?? '' ),
    2304                                         (int) $latest['pass_count'],
    2305                                         (int) $latest['fail_count'],
    2306                                         (int) $latest['unknown_count']
    2307                                 );
     2359                                __( 'Last checked %1$s UTC: %2$d working, %3$d problems, %4$d could not be verified.', 'shipping-contract-guard' ),
     2360                                (string) ( $latest['started_at'] ?? '' ),
     2361                                (int) $latest['pass_count'],
     2362                                (int) $latest['fail_count'],
     2363                                (int) $latest['unknown_count']
     2364                        );
    23082365                }
    23092366                $nextAction = 'none';
    23102367                if ( is_array( $readiness ) ) {
    2311                         $nextAction = (string) ( $readiness['recommended_action'] ?? 'none' );
    2312                         if ( 'verification_due' === ( $readiness['state'] ?? '' ) ) {
     2368                        $nextAction = $latestAttention ? 'review_latest_check' : (string) ( $readiness['recommended_action'] ?? 'none' );
     2369                        if ( ! $latestAttention && 'verification_due' === ( $readiness['state'] ?? '' ) ) {
    23132370                                $key   = 'unknown';
    23142371                                $title = __( 'A shipping check is due', 'shipping-contract-guard' );
    23152372                                $text  = __( 'A relevant store change was observed after the last verification. Run the same approved promises to obtain a fresh verdict.', 'shipping-contract-guard' );
    2316                         } elseif ( 'coverage_attention' === ( $readiness['state'] ?? '' ) ) {
     2373                        } elseif ( ! $latestAttention && 'coverage_attention' === ( $readiness['state'] ?? '' ) ) {
    23172374                                $key   = 'unknown';
    23182375                                $title = __( 'Shipping coverage needs review', 'shipping-contract-guard' );
    … …  
    23242381                $localTimestamp  = is_array( $latest ) ? strtotime( (string) ( $latest['started_at'] ?? '' ) . ' UTC' ) : false;
    23252382                $changeTimestamp = is_array( $readiness ) ? (int) ( $readiness['last_seen'] ?? 0 ) : 0;
    2326                 $localAttention  = is_array( $readiness ) && in_array( (string) ( $readiness['state'] ?? '' ), array( 'verification_due', 'coverage_attention', 'problem', 'could_not_verify' ), true );
     2383                $localAttention  = $latestAttention || ( is_array( $readiness ) && in_array( (string) ( $readiness['state'] ?? '' ), array( 'verification_due', 'coverage_attention', 'problem', 'could_not_verify' ), true ) );
    23272384                $localIsNewer    = $localAttention && false !== $pulseTimestamp && ( ( false !== $localTimestamp && $localTimestamp >= $pulseTimestamp ) || $changeTimestamp >= $pulseTimestamp );
    23282385                $useDailyPulse   = is_array( $dailyPulse ) && ( ! $localIsNewer || in_array( (string) ( $dailyPulse['state'] ?? '' ), array( 'at_risk', 'could_not_verify' ), true ) );
    … …  
    24572514                        <h2 id="scg-current-status"><?php echo esc_html( $title ); ?></h2>
    24582515                        <p><?php echo esc_html( $text ); ?></p>
    2459                         <ul class="scg-dashboard-facts"><li><strong><?php echo esc_html( (string) $protectedCases ); ?></strong> <?php echo esc_html__( 'approved checks', 'shipping-contract-guard' ); ?></li><li><strong><?php echo esc_html( (string) $rule_count ); ?></strong> <?php echo esc_html__( 'protected promises', 'shipping-contract-guard' ); ?></li><li><strong><?php echo esc_html( $lastEvidence ); ?></strong> <?php echo esc_html( $evidenceLabel ); ?></li></ul>
     2516                        <ul class="scg-dashboard-facts"><li><strong><?php echo esc_html( (string) $protectedCases ); ?></strong> <?php echo esc_html( _n( 'approved check', 'approved checks', $protectedCases, 'shipping-contract-guard' ) ); ?></li><li><strong><?php echo esc_html( (string) $rule_count ); ?></strong> <?php echo esc_html( _n( 'protected promise', 'protected promises', $rule_count, 'shipping-contract-guard' ) ); ?></li><li><strong><?php echo esc_html( $lastEvidence ); ?></strong> <?php echo esc_html( $evidenceLabel ); ?></li></ul>
    24602517                        <p class="scg-next-action"><strong><?php echo esc_html__( 'Next action:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $nextActionText ); ?></p>
    24612518                        <?php if ( is_array( $active ) && 'review_latest_check' === $nextAction ) : ?>
    … …  
    25162573        }
    25172574
     2575        /** Prefer a merchant-written promise name; keep opaque case IDs in technical details. */
     2576        private function operator_promise_label( $active, string $caseId ): string {
     2577                $names = array();
     2578                if ( is_array( $active ) ) {
     2579                        $definition = json_decode( (string) ( $active['definition_json'] ?? '' ), true );
     2580                        foreach ( (array) ( $definition['cases'] ?? array() ) as $case ) {
     2581                                if ( ! is_array( $case ) ) {
     2582                                        continue;
     2583                                }
     2584                                $boundary = is_array( $case['boundary'] ?? null ) ? $case['boundary'] : array();
     2585                                $name     = trim( (string) ( $boundary['dimensions']['rule_name'] ?? '' ) );
     2586                                if ( '' !== $name ) {
     2587                                        $names[ $name ] = true;
     2588                                }
     2589                                if ( '' !== $caseId && hash_equals( (string) ( $boundary['case_id'] ?? '' ), $caseId ) && '' !== $name ) {
     2590                                        return $name;
     2591                                }
     2592                        }
     2593                }
     2594                if ( 1 === count( $names ) ) {
     2595                        return (string) array_key_first( $names );
     2596                }
     2597                return __( 'Protected shipping promise', 'shipping-contract-guard' );
     2598        }
     2599
    25182600        /** Offer the shortest safe path through the existing preview and explicit approval flow. */
    2519         private function recommended_setup( $active, array $candidates, array $products ): void {
    2520                 if ( is_array( $active ) || array() === $products ) {
     2601        private function recommended_setup( $active, array $recommendation, array $candidates ): void {
     2602                if ( is_array( $active ) ) {
     2603                        return;
     2604                }
     2605                if ( true !== ( $recommendation['available'] ?? false ) ) {
     2606                        $reasons = array(
     2607                                'no_supported_amount_rule' => __( 'No enabled WooCommerce Free Shipping rule with a simple minimum order amount is available.', 'shipping-contract-guard' ),
     2608                                'no_eligible_product'      => __( 'No published physical product is available for a safe example cart.', 'shipping-contract-guard' ),
     2609                                'postcode_required'        => __( 'The available Free Shipping rule needs a postcode. Shipping Contract Guard will not guess one.', 'shipping-contract-guard' ),
     2610                                'country_not_explicit'     => __( 'The available Free Shipping rule does not name a country that can be used without guessing.', 'shipping-contract-guard' ),
     2611                        );
     2612                        ?>
     2613                        <div class="scg-recommendation scg-recommendation-unavailable" role="note">
     2614                                <h3><?php echo esc_html__( 'Choose the example yourself', 'shipping-contract-guard' ); ?></h3>
     2615                                <p><?php echo esc_html( $reasons[ (string) ( $recommendation['reason'] ?? '' ) ] ?? $reasons['no_supported_amount_rule'] ); ?></p>
     2616                                <p><?php echo esc_html__( 'Open Customize to choose an invented destination and product, or to save the shipping methods and prices WooCommerce returns for one example cart.', 'shipping-contract-guard' ); ?></p>
     2617                        </div>
     2618                        <?php
    25212619                        return;
    25222620                }
    25232621                $recommended = null;
    25242622                foreach ( $candidates as $candidate ) {
    2525                         $hasPostcode = false;
    2526                         foreach ( (array) ( $candidate['zone_locations'] ?? array() ) as $location ) {
    2527                                 if ( is_array( $location ) && 'postcode' === ( $location['type'] ?? null ) ) {
    2528                                         $hasPostcode = true;
    2529                                         break;
    2530                                 }
    2531                         }
    2532                         if ( ! $hasPostcode ) {
     2623                        if ( is_array( $candidate ) && hash_equals( (string) ( $recommendation['candidate_id'] ?? '' ), (string) ( $candidate['candidate_id'] ?? '' ) ) ) {
    25332624                                $recommended = $candidate;
    25342625                                break;
    … …  
    25382629                        return;
    25392630                }
    2540                 $product  = $products[0];
    2541                 $ruleName = substr( $this->candidate_label( $recommended ), 0, 100 );
    2542                 $country  = $this->base_country();
    2543                 foreach ( (array) ( $recommended['zone_locations'] ?? array() ) as $location ) {
    2544                         if ( is_array( $location ) && 'country' === ( $location['type'] ?? null ) && 1 === preg_match( '/^[A-Z]{2}$/', (string) ( $location['code'] ?? '' ) ) ) {
    2545                                 $country = (string) $location['code'];
    2546                                 break;
    2547                         }
    2548                 }
     2631                $ruleName    = substr( $this->candidate_label( $recommended ), 0, 100 );
     2632                $country     = (string) $recommendation['country'];
     2633                $productId   = (int) $recommendation['product_id'];
     2634                $productName = (string) $recommendation['product_name'];
     2635                $threshold   = wp_strip_all_tags( wc_price( (int) $recommendation['threshold_minor'] / ( 10 ** wc_get_price_decimals() ), array( 'currency' => (string) $recommendation['currency'] ) ) );
    25492636                ?>
    25502637                <div class="scg-recommendation" role="note">
    2551                         <p class="scg-eyebrow"><?php echo esc_html__( 'Recommended first protection', 'shipping-contract-guard' ); ?></p>
    2552                         <h3><?php echo esc_html( $ruleName ); ?></h3>
    2553                         <p><?php echo esc_html__( 'Start with one detected commercial boundary and one shippable product. The next screen still shows every expected method and price for explicit approval.', 'shipping-contract-guard' ); ?></p>
     2638                        <p class="scg-eyebrow"><?php echo esc_html__( 'Recommended first promise', 'shipping-contract-guard' ); ?></p>
     2639                        <h3><?php /* translators: 1: formatted free-shipping threshold, 2: country name, 3: example product name. */ echo esc_html( sprintf( __( 'Protect free shipping from %1$s in %2$s using %3$s.', 'shipping-contract-guard' ), $threshold, $this->country_name( $country ), $productName ) ); ?></h3>
     2640                        <p><?php echo esc_html__( 'Before anything is saved, the preview will show the shipping methods and prices WooCommerce returns for this invented example.', 'shipping-contract-guard' ); ?></p>
     2641                        <ul class="scg-boundary-summary" aria-label="<?php echo esc_attr__( 'Cart totals that will be checked', 'shipping-contract-guard' ); ?>">
     2642                        <?php foreach ( (array) $recommendation['boundary_amounts'] as $index => $minor ) : ?>
     2643                                <li><span><?php echo esc_html( array( __( 'Below', 'shipping-contract-guard' ), __( 'At', 'shipping-contract-guard' ), __( 'Above', 'shipping-contract-guard' ) )[ $index ] ?? __( 'Amount', 'shipping-contract-guard' ) ); ?></span><strong><?php echo esc_html( wp_strip_all_tags( wc_price( (int) $minor / ( 10 ** wc_get_price_decimals() ), array( 'currency' => (string) $recommendation['currency'] ) ) ) ); ?></strong></li>
     2644                        <?php endforeach; ?>
     2645                        </ul>
    25542646                        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    25552647                                <input type="hidden" name="action" value="shipping_contract_guard_create_draft">
    … …  
    25582650                                <input type="hidden" name="criticality" value="release_blocking">
    25592651                                <input type="hidden" name="threshold_candidate" value="<?php echo esc_attr( (string) $recommended['candidate_id'] ); ?>">
    2560                                 <input type="hidden" name="product_id" value="<?php echo esc_attr( (string) $product->get_id() ); ?>">
     2652                                <input type="hidden" name="product_id" value="<?php echo esc_attr( (string) $productId ); ?>">
    25612653                                <input type="hidden" name="country" value="<?php echo esc_attr( $country ); ?>">
    25622654                                <input type="hidden" name="postcode_positive" value="">
    25632655                                <input type="hidden" name="postcode_negative" value="">
    25642656                                <?php wp_nonce_field( 'shipping_contract_guard_create_draft' ); ?>
    2565                                 <?php submit_button( __( 'Preview recommended checks', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?>
     2657                                <?php submit_button( __( 'Preview this promise', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?>
    25662658                        </form>
    25672659                </div>
    … …  
    25742666                        $definition = json_decode( (string) $contract['definition_json'], true );
    25752667                        $cases      = is_array( $definition ) && is_array( $definition['cases'] ?? null ) ? $definition['cases'] : array();
    2576                         $heading    = sprintf(
    2577                                 /* translators: %d: generated shipping-check count. */
    2578                                 __( 'Review %d generated checks', 'shipping-contract-guard' ),
    2579                                 count( $cases )
    2580                         );
     2668                        $ruleNames  = array_values( array_unique( array_map( fn( array $case ): string => $this->case_rule_name( $case ), $cases ) ) );
     2669                        $cartTotals = array_values( array_unique( array_map( fn( array $case ): string => wp_strip_all_tags( $this->case_amount( $case ) ), $cases ) ) );
     2670                        $firstCase  = $cases[0] ?? null;
     2671                        $heading    = 1 === count( $ruleNames )
     2672                                ? sprintf(
     2673                                        /* translators: %s: merchant-named shipping promise. */
     2674                                        __( 'Protect %s', 'shipping-contract-guard' ),
     2675                                        $ruleNames[0]
     2676                                )
     2677                                : __( 'Protect these shipping promises', 'shipping-contract-guard' );
    25812678                        ?>
    25822679                        <div class="scg-preview">
    25832680                                <p class="scg-eyebrow"><?php echo esc_html__( 'Preview', 'shipping-contract-guard' ); ?></p>
    2584                                         <h3><?php echo esc_html( $heading ); ?></h3>
    2585                                 <p><?php echo esc_html__( 'These expectations come from your store’s current behavior. Check them before saving; later checks compare WooCommerce against this version.', 'shipping-contract-guard' ); ?></p>
     2681                                <h3><?php echo esc_html( $heading ); ?></h3>
     2682                                <p><?php echo esc_html__( 'Review what WooCommerce returned for this invented example. Nothing becomes protected until you approve it below.', 'shipping-contract-guard' ); ?></p>
     2683                                <?php if ( is_array( $firstCase ) ) : ?>
     2684                                <ul class="scg-preview-summary">
     2685                                        <li><strong><?php echo esc_html__( 'Example cart:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $this->case_cart( $firstCase ) ); ?></li>
     2686                                        <li><strong><?php echo esc_html__( 'Invented destination:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $this->case_destination( $firstCase ) ); ?></li>
     2687                                        <li><strong><?php echo esc_html__( 'Cart totals:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( implode( ', ', $cartTotals ) ); ?></li>
     2688                                        <?php
     2689                                        if ( count( $ruleNames ) > 1 ) :
     2690                                                ?>
     2691                                                <li><strong><?php echo esc_html__( 'Promises:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( implode( ', ', $ruleNames ) ); ?></li><?php endif; ?>
     2692                                </ul>
     2693                                <?php endif; ?>
     2694                                <p class="description"><?php /* translators: %d: exact supporting check count. */ echo esc_html( sprintf( _n( '%d saved check supports this preview.', '%d saved checks support this preview.', count( $cases ), 'shipping-contract-guard' ), count( $cases ) ) ); ?></p>
    25862695                                <table class="widefat striped"><thead><tr><th scope="col"><?php echo esc_html__( 'Protected rule', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Cart', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Cart total', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Test destination', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Expected at checkout', 'shipping-contract-guard' ); ?></th></tr></thead><tbody>
    25872696                                <?php foreach ( $cases as $case ) : ?>
    … …  
    25892698                                <?php endforeach; ?>
    25902699                                </tbody></table>
    2591                                 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"><input type="hidden" name="action" value="shipping_contract_guard_approve_and_run_contract"><input type="hidden" name="contract_id" value="<?php echo esc_attr( (string) $contract['id'] ); ?>"><?php wp_nonce_field( 'shipping_contract_guard_approve_and_run_contract' ); ?><?php submit_button( __( 'Save and run first check', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?></form>
     2700                                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"><input type="hidden" name="action" value="shipping_contract_guard_approve_and_run_contract"><input type="hidden" name="contract_id" value="<?php echo esc_attr( (string) $contract['id'] ); ?>"><?php wp_nonce_field( 'shipping_contract_guard_approve_and_run_contract' ); ?><?php submit_button( __( 'Protect and run first check', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?></form>
    25922701                                <details><summary><?php echo esc_html__( 'Technical definition', 'shipping-contract-guard' ); ?></summary><pre class="scg-technical-definition"><?php echo esc_html( wp_json_encode( $definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); ?></pre></details>
    25932702                        </div>
    … …  
    25962705        }
    25972706
    2598         private function operator_panel( array $rules, array $runs, $connection, $policy, $service_disclosure, $checkout_intent, string $alert_email, $alertTest, array $incidents, array $maintenanceWindows, $activationReceipt ) {
     2707        private function operator_panel( $active, array $rules, array $runs, $connection, $policy, $service_disclosure, $checkout_intent, string $alert_email, $alertTest, array $incidents, array $maintenanceWindows, $activationReceipt ) {
    25992708                $has_value  = false;
    26002709                $latest_run = null;
    … …  
    27242833                                <?php if ( $paid_active ) : ?>
    27252834                                        <div class="scg-operator-incidents"><h3><?php echo esc_html__( 'Shipping verdicts and incidents', 'shipping-contract-guard' ); ?></h3>
    2726                                         <p><?php echo esc_html__( 'Operator keeps one incident open for each failed or unknown promise until a later passing check records recovery.', 'shipping-contract-guard' ); ?></p>
     2835                                        <p><?php echo esc_html__( 'Operator keeps each incident open until a later check confirms that shipping works again.', 'shipping-contract-guard' ); ?></p>
    27272836                                        <?php
    27282837                                        if ( ! $incidents ) :
    … …  
    27302839                                                <p class="description"><?php echo esc_html__( 'Refresh to load the signed offsite verdict. No incident state has been cached locally yet.', 'shipping-contract-guard' ); ?></p><?php endif; ?>
    27312840                                        <?php foreach ( $incidents as $finding ) : ?>
    2732                                                 <?php $incident = is_array( $finding['incident'] ?? null ) ? $finding['incident'] : null; ?>
    2733                                                 <div id="scg-incident-<?php echo esc_attr( sanitize_html_class( (string) $finding['case_id'] ) ); ?>" class="scg-preview"><p><span class="scg-result scg-result-<?php echo esc_attr( (string) $finding['state'] ); ?>"><?php echo esc_html( $this->operator_state_label( (string) $finding['state'] ) ); ?></span> <strong><?php echo esc_html( __( 'Promise', 'shipping-contract-guard' ) . ' ' . (string) $finding['case_id'] ); ?></strong></p>
     2841                                                <?php
     2842                                                $incident     = is_array( $finding['incident'] ?? null ) ? $finding['incident'] : null;
     2843                                                $caseId       = (string) ( $finding['case_id'] ?? '' );
     2844                                                $recoveryRun  = is_array( $incident ) ? (string) ( $incident['recovery_run_id'] ?? '' ) : '';
     2845                                                $promiseLabel = $this->operator_promise_label( $active, $caseId );
     2846                                                ?>
     2847                                                <div id="scg-incident-<?php echo esc_attr( sanitize_html_class( $caseId ) ); ?>" class="scg-preview"><p><span class="scg-result scg-result-<?php echo esc_attr( (string) $finding['state'] ); ?>"><?php echo esc_html( $this->operator_state_label( (string) $finding['state'] ) ); ?></span> <strong><?php echo esc_html( $promiseLabel ); ?></strong></p>
    27342848                                                <?php if ( is_array( $incident ) && 'open' === ( $incident['status'] ?? '' ) ) : ?>
    27352849                                                        <p><?php echo esc_html( empty( $incident['acknowledged_at'] ) ? __( 'Needs acknowledgement', 'shipping-contract-guard' ) : __( 'Acknowledged', 'shipping-contract-guard' ) ); ?>
    … …  
    27412855                                                        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"><input type="hidden" name="action" value="shipping_contract_guard_update_operator_incident"><input type="hidden" name="contract_id" value="<?php echo esc_attr( (string) $finding['contract_id'] ); ?>"><input type="hidden" name="case_id" value="<?php echo esc_attr( (string) $finding['case_id'] ); ?>"><input type="hidden" name="episode_number" value="<?php echo esc_attr( (string) $incident['episode_number'] ); ?>"><?php wp_nonce_field( 'shipping_contract_guard_update_operator_incident' ); ?><label><input type="checkbox" name="acknowledge" value="yes" <?php checked( ! empty( $incident['acknowledged_at'] ) ); ?>> <?php echo esc_html__( 'Acknowledge this incident', 'shipping-contract-guard' ); ?></label><p><label><?php echo esc_html__( 'Owner label', 'shipping-contract-guard' ); ?> <input type="text" name="owner_label" maxlength="80" value="<?php echo esc_attr( (string) ( $incident['owner_label'] ?? '' ) ); ?>"></label></p><p><label><?php echo esc_html__( 'Handoff note', 'shipping-contract-guard' ); ?><br><textarea name="incident_note" maxlength="500" rows="3" class="large-text"><?php echo esc_textarea( (string) ( $incident['note'] ?? '' ) ); ?></textarea></label></p><p class="description"><?php echo esc_html__( 'Use operational labels only. Do not enter customer data, credentials, or secrets.', 'shipping-contract-guard' ); ?></p><?php submit_button( __( 'Save incident handoff', 'shipping-contract-guard' ), 'secondary', 'submit', false ); ?></form>
    27422856                                                <?php elseif ( is_array( $incident ) && 'recovered' === ( $incident['status'] ?? '' ) ) : ?>
    2743                                                         <p><strong><?php echo esc_html__( 'Recovery verified', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( (string) ( $incident['recovered_at'] ?? '' ) ); ?> · <code><?php echo esc_html( (string) ( $incident['recovery_run_id'] ?? '' ) ); ?></code></p>
    2744                                                 <?php endif; ?></div>
     2857                                                        <p><strong><?php /* translators: %s: human-readable UTC recovery time. */ echo esc_html( sprintf( __( 'Recovery verified on %s.', 'shipping-contract-guard' ), $this->dashboard_evidence_time( (string) ( $incident['recovered_at'] ?? '' ) ) ) ); ?></strong></p>
     2858                                                <?php endif; ?>
     2859                                                <details class="scg-technical-reference"><summary><?php echo esc_html__( 'Technical reference', 'shipping-contract-guard' ); ?></summary><p><?php echo esc_html__( 'Case:', 'shipping-contract-guard' ); ?> <code><?php echo esc_html( $caseId ); ?></code>
     2860                                                <?php
     2861                                                if ( '' !== $recoveryRun ) :
     2862                                                        ?>
     2863                                                        <br><?php echo esc_html__( 'Recovery run:', 'shipping-contract-guard' ); ?> <code><?php echo esc_html( $recoveryRun ); ?></code><?php endif; ?></p></details></div>
    27452864                                        <?php endforeach; ?></div>
    27462865                                        <div class="scg-operator-settings"><h3><?php echo esc_html__( 'Monitoring controls', 'shipping-contract-guard' ); ?></h3>
    … …  
    27952914                                <p class="notice notice-warning inline"><?php echo esc_html__( 'Automatic checks are unavailable because the service configuration is incomplete.', 'shipping-contract-guard' ); ?></p>
    27962915                        <?php else : ?>
    2797                                 <div class="scg-upgrade"><p class="scg-eyebrow"><?php echo esc_html__( 'Optional subscription', 'shipping-contract-guard' ); ?></p><h3><?php echo esc_html__( 'Your promises work now. Operator keeps watch when nobody is rerunning them.', 'shipping-contract-guard' ); ?></h3>
    2798                                 <p><?php echo esc_html__( 'Subscribe when shipping changes are frequent, releases happen across a team, or a missed rule needs a named owner and defensible recovery record.', 'shipping-contract-guard' ); ?></p>
     2916                                <div class="scg-upgrade"><p class="scg-eyebrow"><?php echo esc_html__( 'Optional subscription', 'shipping-contract-guard' ); ?></p><h3><?php echo esc_html__( 'You proved this shipping promise now. Operator keeps checking it when nobody is watching.', 'shipping-contract-guard' ); ?></h3>
     2917                                <p><?php echo esc_html__( 'Operator reruns your approved checks after relevant changes and on a safe schedule. It alerts one owner and keeps each problem open until a fresh complete check proves recovery.', 'shipping-contract-guard' ); ?></p>
    27992918                                <div class="scg-operator-compare"><div><strong><?php echo esc_html__( 'Local', 'shipping-contract-guard' ); ?></strong><span><?php echo esc_html__( 'You choose when to run, inspect, export, and follow up.', 'shipping-contract-guard' ); ?></span></div><div><strong><?php echo esc_html__( 'Operator', 'shipping-contract-guard' ); ?></strong><span><?php echo esc_html__( 'Relevant change → verdict → alert → owned incident → verified recovery.', 'shipping-contract-guard' ); ?></span></div></div>
    28002919                                <ul class="scg-value-list"><li><?php echo esc_html__( 'Post-change checks coalesce automatically after a quiet period.', 'shipping-contract-guard' ); ?></li><li><?php echo esc_html__( 'A scheduled heartbeat exposes missed local automation and silent drift.', 'shipping-contract-guard' ); ?></li><li><?php echo esc_html__( 'Failures stay grouped and accountable until a passing run proves recovery.', 'shipping-contract-guard' ); ?></li><li><?php echo esc_html__( 'Signed minimized evidence and 90-day offsite history survive local dashboard cleanup.', 'shipping-contract-guard' ); ?></li></ul>
    … …  
    29543073        }
    29553074
     3075        private function verdict_headline( array $presentation, string $fallback ): string {
     3076                if ( true !== ( $presentation['named'] ?? false ) ) {
     3077                        return $fallback;
     3078                }
     3079                $name = (string) $presentation['promise_name'];
     3080                return match ( (string) ( $presentation['state'] ?? '' ) ) {
     3081                        'pass' => sprintf(
     3082                                /* translators: %s: merchant-named protected shipping promise. */
     3083                                __( '%s is working for this saved example.', 'shipping-contract-guard' ),
     3084                                $name
     3085                        ),
     3086                        'fail' => sprintf(
     3087                                /* translators: %s: merchant-named protected shipping promise. */
     3088                                __( '%s may not be working for this saved example.', 'shipping-contract-guard' ),
     3089                                $name
     3090                        ),
     3091                        'unknown' => sprintf(
     3092                                /* translators: %s: merchant-named protected shipping promise. */
     3093                                __( '%s could not be checked safely for this saved example.', 'shipping-contract-guard' ),
     3094                                $name
     3095                        ),
     3096                        'stale' => sprintf(
     3097                                /* translators: %s: merchant-named protected shipping promise. */
     3098                                __( '%s needs a fresh check for this saved example.', 'shipping-contract-guard' ),
     3099                                $name
     3100                        ),
     3101                        default => $fallback,
     3102                };
     3103        }
     3104
     3105        private function verdict_consequence( array $presentation, array $run ): string {
     3106                $checked = (int) ( $run['pass_count'] ?? 0 ) + (int) ( $run['fail_count'] ?? 0 ) + (int) ( $run['unknown_count'] ?? 0 );
     3107                return match ( (string) ( $presentation['state'] ?? '' ) ) {
     3108                        'pass' => sprintf(
     3109                                /* translators: %d: finite number of saved example checks. */
     3110                                _n( 'WooCommerce matched the saved method and price in %d check. This result covers only the saved example.', 'WooCommerce matched the saved methods and prices in all %d checks. This result covers only the saved examples.', $checked, 'shipping-contract-guard' ),
     3111                                $checked
     3112                        ),
     3113                        'fail' => sprintf(
     3114                                /* translators: %d: number of checks that differed from the saved result. */
     3115                                _n( '%d check differed from the saved result. A shopper matching this example may see different shipping.', '%d checks differed from the saved result. Shoppers matching these examples may see different shipping.', (int) ( $run['fail_count'] ?? 0 ), 'shipping-contract-guard' ),
     3116                                (int) ( $run['fail_count'] ?? 0 )
     3117                        ),
     3118                        'unknown' => sprintf(
     3119                                /* translators: %d: number of checks that could not be completed safely. */
     3120                                _n( '%d check could not be completed safely. It is not counted as working.', '%d checks could not be completed safely. They are not counted as working.', (int) ( $run['unknown_count'] ?? 0 ), 'shipping-contract-guard' ),
     3121                                (int) ( $run['unknown_count'] ?? 0 )
     3122                        ),
     3123                        'stale' => __( 'A relevant store change happened after this result. Run the same saved example again before relying on it.', 'shipping-contract-guard' ),
     3124                        default => __( 'Review the saved example and its latest WooCommerce result.', 'shipping-contract-guard' ),
     3125                };
     3126        }
     3127
     3128        private function verdict_scope( array $scope ): string {
     3129                $parts   = array();
     3130                $product = $this->product_name( (int) ( $scope['product_id'] ?? 0 ) );
     3131                if ( '' !== $product ) {
     3132                        $parts[] = $product;
     3133                }
     3134                $country = (string) ( $scope['country'] ?? '' );
     3135                if ( '' !== $country ) {
     3136                        $destination = $this->country_name( $country );
     3137                        if ( '' !== (string) ( $scope['postcode'] ?? '' ) ) {
     3138                                $destination .= ' ' . (string) $scope['postcode'];
     3139                        }
     3140                        $parts[] = $destination;
     3141                }
     3142                $currency = (string) ( $scope['currency'] ?? '' );
     3143                if ( '' !== $currency ) {
     3144                        $parts[] = sprintf(
     3145                                /* translators: %s: formatted invented cart total. */
     3146                                __( 'cart total %s', 'shipping-contract-guard' ),
     3147                                wp_strip_all_tags( wc_price( (int) ( $scope['amount_minor'] ?? 0 ) / ( 10 ** wc_get_price_decimals() ), array( 'currency' => $currency ) ) )
     3148                        );
     3149                }
     3150                return implode( ' · ', $parts );
     3151        }
     3152
    29563153        private function trigger_label( string $trigger ): string {
    29573154                $labels = array(
    … …  
    32833480                }
    32843481        }
     3482
     3483        private function country_name( string $country ): string {
     3484                $country = strtoupper( $country );
     3485                try {
     3486                        $countries = WC()->countries->get_countries();
     3487                        return isset( $countries[ $country ] ) ? (string) $countries[ $country ] : $country;
     3488                } catch ( \Throwable $exception ) {
     3489                        return $country;
     3490                }
     3491        }
    32853492}
  • shipping-contract-guard/tags/1.9.0/src/Application/ThresholdCandidateCompiler.php

    r3649397 r3650174  
    6565                        }
    6666                }
    67                 throw new ApplicationException( 'The selected threshold is no longer present in the current WooCommerce configuration.' );
     67                throw new ApplicationException( 'The selected Free Shipping rule is no longer available in the current WooCommerce settings.' );
    6868        }
    6969
  • shipping-contract-guard/trunk/readme.txt

    r3649397 r3650174  
    44Tested up to: 7.0
    55Requires PHP: 8.1
    6 Stable tag: 1.8.1
     6Stable tag: 1.9.0
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1212== Description ==
    1313
    14 Shipping Contract Guard turns important WooCommerce shipping results into repeatable, manager-approved checks.
     14Shipping Contract Guard checks whether the shipping options and prices your store promises still work.
    1515
    16 Start with one shipping promise. For example, if free shipping begins at EUR 75, the plugin checks synthetic carts immediately below, exactly at, and immediately above that boundary. You review the methods and prices WooCommerce returns before saving anything. Later checks compare against that approved version; current behavior is never silently treated as the new baseline.
     16Start with one product, one destination, and one expected shipping result. When the plugin finds a supported rule, it recommends three invented cart totals: just below the boundary, exactly at it, and just above it. You see what WooCommerce returns before you approve the check. Every later run compares the current result with your approved promise, so a changed result cannot silently become normal.
     17
     18The dashboard gives you one clear status: **Everything works**, **Problem found**, or **Could not verify**. When something differs, it explains the shopper impact and the next thing to check. The plugin does this without creating a checkout, customer, or order.
    1719
    1820Local mode is free and requires no account:
    1921
    2022* protect supported WooCommerce core Flat Rate, Free Shipping, and Local Pickup boundaries;
    21 * protect an exact public result returned by another configured shipping method after approving the disclosed quote behavior;
     23* save an exact shipping option and price returned by an enabled method after reviewing how the check will run;
    2224* run invented carts without creating a checkout, customer, or order;
    2325* distinguish **Everything works**, **Problem found**, and **Could not verify**;
    24 * review finite coverage, recent changes, and likely affected approved checks;
    25 * keep 90 days of bounded local history and export JSON, CSV, or a human-readable assurance report; and
    26 * preview and apply one exact core-method availability change with full-contract verification and automatic rollback;
    27 * capture and restore one exact passing core-method state; and
    28 * activate an expiring local continuity path with verified restoration.
     26* review what is covered, recent store changes, and which approved checks may be affected;
     27* keep up to 90 days of local check history and export JSON, CSV, or a readable assurance report;
     28* preview turning one supported WooCommerce shipping option on or off, run every approved check, and roll back automatically if needed;
     29* save one passing setup and restore it later; or
     30* activate a time-limited recovery plan and confirm when the original setup works again.
    2931
    30 Operator is an optional EUR 29/month service for one store. It adds scheduled and post-change checks, independent missed-run supervision, email alerts, minimized offsite evidence, an accountable incident, and verified recovery. A successful checkout page never grants access; only verified billing events and a fresh signed site policy authorize paid work.
     32Operator is an optional EUR 29/month service for one store. It adds scheduled checks, checks after changes, missed-run supervision, email alerts, a named incident owner, and verified recovery. It sends only the limited evidence described below. Returning from checkout does not start paid service. Operator starts only after Stripe confirms the subscription and the plugin receives current service authorization.
    3133
    3234= Deliberate limits =
    3335
    34 Coverage is finite. Unsupported methods, unsafe scenarios, incomplete calculations, and ambiguous evidence remain **Could not verify**. Shipping Contract Guard does not claim that every checkout path is covered or that a preceding configuration change caused a failure.
     36Coverage has limits. Unsupported methods, unsafe scenarios, incomplete calculations, and unclear evidence remain **Could not verify**. Shipping Contract Guard does not claim that every checkout path is covered or that an earlier settings change caused a failure.
    3537
    36 Core suggestions use only public WooCommerce configuration. Provider-agnostic promises use WooCommerce's public returned-rate boundary. The plugin never reads carrier credentials or private provider settings. An explicitly approved public-rate check may cause the configured method to make its ordinary quote request for the invented cart and destination.
     38Core suggestions read only supported WooCommerce settings. For other enabled methods, a saved check uses only the shipping options and prices WooCommerce returns. The plugin never reads carrier credentials or private provider settings. Running an approved check may cause the shipping method to request its normal quote for the invented cart and destination.
    3739
    38 The plugin does not create orders, submit checkout, process payments, buy labels, contact shoppers, or grant remote control over shipping. Its guarded write boundary is limited to the enabled state of one explicitly previewed WooCommerce core Flat Rate, Free Shipping, or Local Pickup zone-method instance. Prices, thresholds, destinations, zone order, products, coupons, provider settings, orders, payments, and labels remain outside that boundary.
     40The plugin does not create orders, submit checkout, process payments, buy labels, contact shoppers, or grant remote control over shipping. After you preview and approve the action, it can turn one supported WooCommerce Flat Rate, Free Shipping, or Local Pickup option on or off. It cannot change prices, thresholds, destinations, zone order, products, coupons, provider settings, orders, payments, or labels.
    3941
    4042== Installation ==
    … …  
    44463. Open **WooCommerce > Shipping Contract Guard**.
    45474. Choose **Preview recommended checks** when a supported rule is detected. Otherwise open **Set up your first protected rule**.
    46 5. Review the generated synthetic carts, expected methods, and prices.
     485. Review the invented carts, expected shipping options, and prices.
    47496. Choose **Save and run first check**.
    4850
    49 The first check should take only a few minutes. Planning, coverage, recovery, and agency tools stay inside the Advanced sections and are not required for initial setup.
     51Planning, coverage, recovery, and agency tools stay inside the Advanced sections. You do not need them for the first check.
    5052
    5153Deployment tooling may run `wp shipping-contract-guard verify`. Exit 0 means safe, 2 means blocked, 3 means the evidence could not decide, and setup or execution errors use exit 1.
    … …  
    5355== Screenshots ==
    5456
    55 1. Protect one detected shipping boundary with a recommended first check.
    56 2. Review every generated synthetic cart, method, and price before approval.
    57 3. See today's plain-language shipping status and one next action.
    58 4. Review bounded check history and detailed expected-versus-observed evidence.
     571. See a named shipping problem, its shopper impact, and the next thing to check.
     582. Review the recommended totals and expected shipping result before saving anything.
     593. Give an Operator incident a clear owner while technical references stay out of the way.
     604. Confirm that the same protected shipping promise passes again after recovery.
    5961== External services ==
    6062
    … …  
    6567= Optional Shipping Contract Guard Operator =
    6668
    67 The plugin makes no Shipping Contract Guard service request until a WooCommerce manager accepts the fixed disclosure. Starting Operator generates an opaque site-bound checkout reference and private signing key locally, then opens Stripe-hosted checkout. The private signing key never goes to Stripe. Verified Stripe subscription and invoice events, not a redirect, establish entitlement.
     69The plugin does not contact the Shipping Contract Guard service until a WooCommerce manager accepts the disclosure shown in the plugin. Starting Operator creates a private signing key and a checkout reference tied to the site, then opens Stripe-hosted checkout. The private signing key never goes to Stripe. Returning from checkout does not start the service; a verified Stripe subscription or invoice event is required.
    6870
    69 After pairing, the plugin contacts `https://api.shippingcontractguard.com` to pair or revoke the site, rotate its public signing identity, retrieve signed policy, configure the selected schedule and alert email, deliver minimized signed run evidence, retrieve current supervision state, and update a manager-submitted incident handoff.
     71After pairing, the plugin contacts `https://api.shippingcontractguard.com` to connect or disconnect the site, refresh its signed authorization, configure the selected schedule and alert email, send the limited check results listed below, retrieve the current monitoring state, and update an incident handoff submitted by a manager.
    7072
    71 Transfers can contain opaque site, contract, run, and case identifiers; a public signing key; timestamps and nonces; configuration fingerprints; pass, fail, unknown, and completeness states; supported boundary kinds and core method classes; hashed method-instance identities; bounded amounts and currency; error codes; and bounded configuration-change facts. A submitted incident can also contain acknowledgement, an operational owner label, and a bounded note. Do not put customer data, credentials, or secrets in those fields.
     73Transfers can contain internal identifiers for the site, saved promise, run, and check; a public signing key; timestamps and one-time request values; cryptographic fingerprints of the shipping configuration; result and completeness states; supported boundary and shipping-method types; hashed method identities; limited cart amounts and currency; error codes; and a limited description of relevant configuration changes. A submitted incident can also contain an acknowledgement, an operational owner label, and a short note. Do not put customer data, credentials, or secrets in those fields.
    7274
    73 The connector excludes products, raw destinations, customers, orders, payment details, merchant payment-processor data, and carrier credentials. The service retains evidence according to the subscribed plan, initially 90 days. Disconnecting stops new paid work and transfer. An organization owner can export data or request deletion; deletion remains cancelable for 24 hours before tenant records are purged, apart from a minimal replay-protection tombstone.
     75The connector excludes products, full destinations, customers, orders, payment details, merchant payment-processor data, and carrier credentials. The service retains evidence according to the subscribed plan, initially 90 days. Disconnecting stops new paid work and data transfer. An organization owner can export data or request deletion. A deletion request can be canceled for 24 hours; afterward, the service removes the organization's records but keeps a minimal record that prevents an old request from being reused.
    7476
    7577Privacy policy: https://shippingcontractguard.com/privacy.html
    … …  
    8183== Privacy ==
    8284
    83 Local data includes approved contract definitions, synthetic case results, configuration fingerprints and bounded change facts, run timestamps, local operational decisions, and optional service state. Completed local runs are retained for up to 90 days and 500 records. Deactivation preserves data. Uninstall preserves data unless an administrator explicitly enables delete-on-uninstall.
     85Local data includes approved shipping promises, invented-cart results, cryptographic fingerprints and limited facts about settings changes, run times, local incident decisions, and optional service state. Completed local runs are retained for up to 90 days and 500 records. Deactivation preserves data. Uninstall preserves data unless an administrator explicitly enables delete-on-uninstall.
    8486
    8587The plugin adds suggested text to **Settings > Privacy**. Use invented test products and destinations; never enter customer data, credentials, or secrets. A privacy-safe diagnostic download contains only versions, schema and scheduler state, connection presence, and aggregate latest-run counts. Optional WooCommerce logging is off by default and records only an admin action name plus success or error outcome.
    … …  
    8991= Does local verification require an account? =
    9092
    91 No. Contract setup, approval, manual checks, local history, diagnostics, and exports work without an account or Shipping Contract Guard service request.
     93No. Creating and approving checks, running them manually, viewing local history, using diagnostics, and exporting results all work without an account or Shipping Contract Guard service request.
    9294
    93 = Does this inspect my shipping plugin? =
     95= Does this read private carrier settings? =
    9496
    95 No. Core suggestions support WooCommerce core methods. A provider-agnostic promise records only method identities and prices returned through WooCommerce's public shipping calculation after explicit approval.
     97No. Core suggestions use WooCommerce core settings. A public-result promise records only the shipping method identities and prices returned by WooCommerce after you approve the check.
    9698
    9799= Does this test checkout or payment? =
    98100
    99 No. It calculates isolated synthetic shipping packages without submitting checkout or creating an order. Stripe is contacted only when a manager chooses the optional Operator subscription.
     101No. It asks WooCommerce for shipping results for an invented cart. It does not open or submit checkout, and it creates no order. Stripe is contacted only when a manager chooses the optional Operator subscription.
    100102
    101103= What does Could not verify mean? =
    … …  
    109111= Are Cart and Checkout blocks supported? =
    110112
    111 Current block checkout is included in the finite browser matrix. WooCommerce 8.2 block rendering is excluded because that version creates a checkout-draft order merely by rendering, which violates the plugin's zero-order test invariant. Classic checkout remains covered at the minimum boundary.
     113Yes. Current Cart and Checkout blocks are covered by our browser tests. For WooCommerce 8.2, we test classic checkout only because merely opening its block checkout creates a draft order; our tests must never create orders.
    112114
    113115== Changelog ==
     116
     117= 1.9.0 =
     118
     119* Makes the first protected promise a clear recommendation with one product, one location, and three boundary totals.
     120* Keeps custom setup available under a plain “Customize” control.
     121* Leads each result with the protected promise, its scope, the consequence, and one next check.
     122* Adds test coverage for approved shipping results returned by WooCommerce.
    114123
    115124= 1.8.1 =
    … …  
    122131* Simplified the primary manager navigation and moved planning, coverage, and guarded-change workflows behind Advanced sections.
    123132* Added privacy-safe Site Health information, a diagnostic download, opt-in bounded WooCommerce logging, and direct documentation/support links.
    124 * Added an exact core-method change preview, passing-evidence known-good vault, and bounded continuity playbook.
    125 * Kept guarded core-method application and continuity local and free. Operator remains the optional external supervision service; no price, threshold, zone-order, opaque-provider, order, payment, or remote-command authority was added.
     133* Added a reviewed on/off change for one supported WooCommerce shipping option, a saved passing setup, and a time-limited recovery plan.
     134* Kept these recovery tools local and free. Operator remains optional and cannot change prices, thresholds, destinations, zone order, provider settings, orders, payments, or labels.
    126135
    127136= 1.7.2 =
  • shipping-contract-guard/trunk/shipping-contract-guard.php

    r3649397 r3650174  
    33 * Plugin Name:       Shipping Contract Guard for WooCommerce
    44 * Description:       Catch broken WooCommerce shipping rules before customers do.
    5  * Version:           1.8.1
     5 * Version:           1.9.0
    66 * Requires at least: 6.5
    77 * Requires PHP:      8.1
    … …  
    2222defined( 'ABSPATH' ) || exit;
    2323
    24 define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1.8.1' );
     24define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1.9.0' );
    2525define( 'SHIPPING_CONTRACT_GUARD_FILE', __FILE__ );
    2626define( 'SHIPPING_CONTRACT_GUARD_DIR', plugin_dir_path( __FILE__ ) );
  • shipping-contract-guard/trunk/src/Admin/Actions.php

    r3649397 r3650174  
    945945                        $criticality       = isset( $request['criticality'] ) ? sanitize_key( $request['criticality'] ) : 'release_blocking';
    946946                        if ( 1 !== preg_match( '/^[A-Z]{2}$/', $country ) || $product < 1 || strlen( $postcode_positive ) > 20 || strlen( $postcode_negative ) > 20 || strlen( $coupon ) > 100 || '' === $rule_name || strlen( $rule_name ) > 100 || '' === $group_name || strlen( $group_name ) > 80 || ! in_array( $criticality, array( 'advisory', 'standard', 'release_blocking' ), true ) || 1 !== preg_match( '/^[a-f0-9]{64}$/', $candidate ) ) {
    947                                 throw new \InvalidArgumentException( 'Country, synthetic product, coupon, or configured threshold selection is invalid.' );
     947                                throw new \InvalidArgumentException( 'The country, test product, coupon, or selected Free Shipping rule is invalid.' );
    948948                        }
    949949                        $primary = $this->shippable_product( $product );
  • shipping-contract-guard/trunk/src/Admin/Assets.php

    r3649397 r3650174  
    1818                wp_add_inline_style(
    1919                        'shipping-contract-guard-admin',
    20                         '.shipping-contract-guard-wrap{max-width:1080px}.scg-page-header{padding:24px 0 10px}.scg-page-header h1{font-size:34px;letter-spacing:-.02em}.scg-eyebrow{margin:0 0 4px;color:#3858a7;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.scg-lead,.scg-section-lead{max-width:760px;font-size:17px;color:#50575e}.scg-section-lead{margin-top:-4px}.scg-trust-list{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 0;padding:0;list-style:none}.scg-trust-list li{padding:5px 10px;border:1px solid #c3c4c7;border-radius:999px;background:#fff;color:#3c434a;font-size:12px;font-weight:600}.scg-status-card,.scg-panel,.scg-preview,.scg-upgrade{margin:20px 0;padding:24px;background:#fff;border:1px solid #dcdcde;border-radius:10px;box-shadow:0 1px 2px rgba(0,0,0,.03)}.scg-status-card{border-left-width:5px}.scg-status-setup,.scg-status-ready,.scg-status-running,.scg-status-unknown{border-left-color:#dba617}.scg-status-pass{border-left-color:#008a20}.scg-status-fail{border-left-color:#d63638}.scg-status-card h2,.scg-panel>h2{margin-top:4px}.scg-dashboard-facts{display:flex;gap:12px;flex-wrap:wrap;margin:18px 0;padding:0;list-style:none}.scg-dashboard-facts li{min-width:150px;padding:12px 14px;border-radius:8px;background:#f6f7f7}.scg-dashboard-facts strong{display:block;font-size:18px}.scg-tabs{margin:24px 0 0}.scg-example{margin:16px 0;padding:14px;background:#f0f6fc;border-left:4px solid #2271b1}.scg-recommendation{margin:18px 0;padding:18px;border:1px solid #72aee6;border-radius:8px;background:#f0f6fc}.scg-recommendation h3{margin:4px 0}.scg-coverage-group>.scg-panel:first-child{margin-top:20px}.scg-advanced-tools{margin:20px 0;padding:16px 20px;border:1px solid #c3c4c7;border-radius:10px;background:#f6f7f7}.scg-advanced-tools>summary{display:flex;min-height:24px;align-items:center;cursor:pointer;font-size:16px;font-weight:600}.scg-setup-form{max-width:780px}.scg-step{display:grid;grid-template-columns:38px 1fr;gap:14px;margin:24px 0}.scg-step-number{display:flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:50%;background:#2271b1;color:#fff;font-weight:700}.scg-step label,.scg-step legend{display:block;margin-bottom:6px}.scg-step select{width:100%;max-width:620px;color:#1d2327!important;background:#fff!important}.scg-step input[type=text],.scg-step input:not([type]){max-width:300px}.scg-step fieldset{border:0;padding:0}.scg-step fieldset label{margin-top:12px}.scg-preview{border-color:#8c8f94}.scg-preview table{margin:14px 0}.scg-technical-definition{max-height:24rem;overflow:auto;white-space:pre-wrap}.scg-result{display:inline-block;padding:4px 9px;border-radius:999px;font-weight:600}.scg-result-pass{background:#edfaef;color:#006b1b}.scg-result-fail{background:#fcf0f1;color:#8a2424}.scg-result-unknown,.scg-result-running{background:#fcf5e8;color:#6f4f00}.scg-history td,.scg-run-report td{vertical-align:middle}.scg-panel details{margin:16px 0}.scg-panel summary{cursor:pointer;font-weight:600}.scg-run-report{padding:16px;border:1px solid #c3c4c7;border-radius:8px}.scg-run-report table{margin-top:14px}.scg-change-report{margin-top:18px;padding:12px 16px;background:#f6f7f7}.scg-upgrade{background:#f0f6fc;border-color:#72aee6}.scg-twin{border-color:#72aee6;background:#f8fbff;color:#1d2327}.scg-twin-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:22px}.scg-twin-card{padding:18px;border:1px solid #dcdcde;border-radius:9px;background:#fff}.scg-twin-card h3{margin:4px 0 8px}.scg-twin-card label{display:block;margin:10px 0 4px;font-weight:600}.scg-twin-card select,.scg-twin-card input[type=number]{width:100%;max-width:430px;color:#1d2327!important;background:#fff!important}.scg-twin-card .button{margin-top:12px}.scg-twin-budget{grid-column:1/-1}.scg-twin-list{margin:12px 0 0}.scg-twin-list li{margin:8px 0}.scg-twin-list span{display:block;color:#50575e;font-size:12px}.scg-twin-result{margin-top:14px;padding:12px;border-left:4px solid #3858a7;background:#f0f6fc}.scg-recovery{border-left:5px solid #3858a7}.scg-recovery-controls{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:22px 0}.scg-recovery-controls>div,.scg-recovery-result{padding:18px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-recovery-controls label,.scg-recovery-preview label{display:block;margin:10px 0 4px;font-weight:600}.scg-recovery-controls select,.scg-recovery-preview textarea{width:100%;max-width:520px}.scg-recovery-controls select,.scg-review-queue select{color:#1d2327!important;background:#fff!important}.scg-review-queue{margin:18px 0}.scg-review-queue>li{margin:14px 0;padding:16px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-review-queue label{display:block;margin:10px 0 4px;font-weight:600}.scg-review-queue select,.scg-review-queue textarea{width:100%;max-width:520px}.scg-recovery-preview form{margin-top:16px}.scg-digest{overflow-wrap:anywhere}.scg-inline-fields{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.scg-inline-fields input{display:block;max-width:130px}.scg-operator-facts,.scg-operator-compare{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}.scg-operator-facts>div,.scg-operator-compare>div{padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#fff}.scg-operator-facts strong,.scg-operator-facts span,.scg-operator-compare strong,.scg-operator-compare span{display:block}.scg-operator-facts strong{overflow-wrap:anywhere;font-size:15px}.scg-operator-facts span,.scg-operator-compare span{margin-top:4px;color:#50575e}.scg-operator-compare{grid-template-columns:1fr 1fr}.scg-operator-compare>div:last-child{border-color:#72aee6}.scg-value-list li{margin:8px 0}.scg-operator-gate{padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-activation-checklist{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:18px 0;padding:0;list-style:none;counter-reset:activation}.scg-activation-checklist li{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:start;padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7;counter-increment:activation}.scg-activation-checklist li>span:last-child{display:grid;gap:3px;color:#50575e}.scg-activation-checklist strong{color:#1d2327}.scg-operator-incidents{margin:18px 0;padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-operator-settings{margin-top:22px;padding-top:18px;border-top:1px solid #dcdcde}@media(max-width:782px){.scg-step{grid-template-columns:1fr}.scg-step-number{margin-bottom:4px}.scg-history,.scg-run-report table{display:block;overflow-x:auto}.scg-dashboard-facts,.scg-trust-list{display:block}.scg-dashboard-facts li,.scg-trust-list li{display:table;margin:6px 0}.scg-twin-grid,.scg-recovery-controls,.scg-activation-checklist{grid-template-columns:1fr}.scg-twin-budget{grid-column:auto}.scg-operator-facts,.scg-operator-compare{grid-template-columns:1fr}}'
     20                        '.shipping-contract-guard-wrap{max-width:1080px}.scg-page-header{padding:24px 0 10px}.scg-page-header h1{font-size:34px;letter-spacing:-.02em}.scg-eyebrow{margin:0 0 4px;color:#3858a7;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.scg-lead,.scg-section-lead{max-width:760px;font-size:17px;color:#50575e}.scg-section-lead{margin-top:-4px}.scg-trust-list{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 0;padding:0;list-style:none}.scg-trust-list li{padding:5px 10px;border:1px solid #c3c4c7;border-radius:999px;background:#fff;color:#3c434a;font-size:12px;font-weight:600}.scg-status-card,.scg-panel,.scg-preview,.scg-upgrade{margin:20px 0;padding:24px;background:#fff;border:1px solid #dcdcde;border-radius:10px;box-shadow:0 1px 2px rgba(0,0,0,.03)}.scg-status-card{border-left-width:5px}.scg-status-setup,.scg-status-ready,.scg-status-running,.scg-status-unknown{border-left-color:#dba617}.scg-status-pass{border-left-color:#008a20}.scg-status-fail{border-left-color:#d63638}.scg-status-card h2,.scg-panel>h2{margin-top:4px}.scg-dashboard-facts{display:flex;gap:12px;flex-wrap:wrap;margin:18px 0;padding:0;list-style:none}.scg-dashboard-facts li{min-width:150px;padding:12px 14px;border-radius:8px;background:#f6f7f7}.scg-dashboard-facts strong{display:block;font-size:18px}.scg-tabs{margin:24px 0 0}.scg-example{margin:16px 0;padding:14px;background:#f0f6fc;border-left:4px solid #2271b1}.scg-recommendation{margin:18px 0;padding:18px;border:1px solid #72aee6;border-radius:8px;background:#f0f6fc}.scg-recommendation h3{margin:4px 0}.scg-boundary-summary{display:grid;grid-template-columns:repeat(3,minmax(110px,1fr));gap:10px;max-width:620px;margin:16px 0;padding:0;list-style:none}.scg-boundary-summary li{padding:10px 12px;border:1px solid #c3c4c7;border-radius:7px;background:#fff}.scg-boundary-summary span,.scg-boundary-summary strong{display:block}.scg-boundary-summary span{color:#50575e;font-size:12px}.scg-preview-summary{margin:14px 0;padding:14px 18px 14px 34px;background:#f6f7f7}.scg-preview-summary li{margin:5px 0}.scg-promise-verdict{margin:16px 0;padding:16px;border-left:4px solid #3858a7;background:#f0f6fc}.scg-promise-verdict h3{margin-top:0}.scg-coverage-group>.scg-panel:first-child{margin-top:20px}.scg-advanced-tools{margin:20px 0;padding:16px 20px;border:1px solid #c3c4c7;border-radius:10px;background:#f6f7f7}.scg-advanced-tools>summary{display:flex;min-height:24px;align-items:center;cursor:pointer;font-size:16px;font-weight:600}.scg-setup-form{max-width:780px}.scg-step{display:grid;grid-template-columns:38px 1fr;gap:14px;margin:24px 0}.scg-step-number{display:flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:50%;background:#2271b1;color:#fff;font-weight:700}.scg-step label,.scg-step legend{display:block;margin-bottom:6px}.scg-step select{width:100%;max-width:620px;color:#1d2327!important;background:#fff!important}.scg-step input[type=text],.scg-step input:not([type]){max-width:300px}.scg-step fieldset{border:0;padding:0}.scg-step fieldset label{margin-top:12px}.scg-preview{border-color:#8c8f94}.scg-preview table{margin:14px 0}.scg-technical-definition{max-height:24rem;overflow:auto;white-space:pre-wrap}.scg-result{display:inline-block;padding:4px 9px;border-radius:999px;font-weight:600}.scg-result-pass{background:#edfaef;color:#006b1b}.scg-result-fail{background:#fcf0f1;color:#8a2424}.scg-result-unknown,.scg-result-running{background:#fcf5e8;color:#6f4f00}.scg-history td,.scg-run-report td{vertical-align:middle}.scg-panel details{margin:16px 0}.scg-panel summary{cursor:pointer;font-weight:600}.scg-run-report{padding:16px;border:1px solid #c3c4c7;border-radius:8px}.scg-run-report table{margin-top:14px}.scg-change-report{margin-top:18px;padding:12px 16px;background:#f6f7f7}.scg-upgrade{background:#f0f6fc;border-color:#72aee6}.scg-twin{border-color:#72aee6;background:#f8fbff;color:#1d2327}.scg-twin-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:22px}.scg-twin-card{padding:18px;border:1px solid #dcdcde;border-radius:9px;background:#fff}.scg-twin-card h3{margin:4px 0 8px}.scg-twin-card label{display:block;margin:10px 0 4px;font-weight:600}.scg-twin-card select,.scg-twin-card input[type=number]{width:100%;max-width:430px;color:#1d2327!important;background:#fff!important}.scg-twin-card .button{margin-top:12px}.scg-twin-budget{grid-column:1/-1}.scg-twin-list{margin:12px 0 0}.scg-twin-list li{margin:8px 0}.scg-twin-list span{display:block;color:#50575e;font-size:12px}.scg-twin-result{margin-top:14px;padding:12px;border-left:4px solid #3858a7;background:#f0f6fc}.scg-recovery{border-left:5px solid #3858a7}.scg-recovery-controls{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:22px 0}.scg-recovery-controls>div,.scg-recovery-result{padding:18px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-recovery-controls label,.scg-recovery-preview label{display:block;margin:10px 0 4px;font-weight:600}.scg-recovery-controls select,.scg-recovery-preview textarea{width:100%;max-width:520px}.scg-recovery-controls select,.scg-review-queue select{color:#1d2327!important;background:#fff!important}.scg-review-queue{margin:18px 0}.scg-review-queue>li{margin:14px 0;padding:16px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7}.scg-review-queue label{display:block;margin:10px 0 4px;font-weight:600}.scg-review-queue select,.scg-review-queue textarea{width:100%;max-width:520px}.scg-recovery-preview form{margin-top:16px}.scg-digest{overflow-wrap:anywhere}.scg-inline-fields{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.scg-inline-fields input{display:block;max-width:130px}.scg-operator-facts,.scg-operator-compare{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}.scg-operator-facts>div,.scg-operator-compare>div{padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#fff}.scg-operator-facts strong,.scg-operator-facts span,.scg-operator-compare strong,.scg-operator-compare span{display:block}.scg-operator-facts strong{overflow-wrap:break-word;word-break:normal;font-size:15px}.scg-operator-facts span,.scg-operator-compare span{margin-top:4px;color:#50575e}.scg-operator-compare{grid-template-columns:1fr 1fr}.scg-operator-compare>div:last-child{border-color:#72aee6}.scg-value-list li{margin:8px 0}.scg-operator-gate{padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-activation-checklist{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:18px 0;padding:0;list-style:none;counter-reset:activation}.scg-activation-checklist li{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:start;padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7;counter-increment:activation}.scg-activation-checklist li>span:last-child{display:grid;gap:3px;color:#50575e}.scg-activation-checklist strong{color:#1d2327}.scg-operator-incidents{margin:18px 0;padding:18px;border:1px solid #c3c4c7;border-radius:8px;background:#f6f7f7}.scg-technical-reference{font-size:12px}.scg-operator-settings{margin-top:22px;padding-top:18px;border-top:1px solid #dcdcde}@media(max-width:1200px){.scg-operator-facts{grid-template-columns:repeat(2,1fr)}}@media(max-width:782px){.scg-step{grid-template-columns:1fr}.scg-step-number{margin-bottom:4px}.scg-history,.scg-run-report table{display:block;overflow-x:auto}.scg-dashboard-facts,.scg-trust-list{display:block}.scg-dashboard-facts li,.scg-trust-list li{display:table;margin:6px 0}.scg-boundary-summary{grid-template-columns:1fr}.scg-twin-grid,.scg-recovery-controls,.scg-activation-checklist{grid-template-columns:1fr}.scg-twin-budget{grid-column:auto}.scg-operator-facts,.scg-operator-compare{grid-template-columns:1fr}}'
    2121                );
    2222                wp_add_inline_style(
  • shipping-contract-guard/trunk/src/Admin/Page.php

    r3649397 r3650174  
    33namespace ShippingContractGuard\Admin;
    44
     5use ShippingContractGuard\Application\FirstPromiseRecommendation;
     6use ShippingContractGuard\Application\PromiseVerdictPresentation;
    57use ShippingContractGuard\LocalApplication;
    68use ShippingContractGuard\Lifecycle\ConfigurationChangeAutomation;
    … …  
    219221                $notice = get_transient( 'shipping_contract_guard_notice_' . get_current_user_id() );
    220222                delete_transient( 'shipping_contract_guard_notice_' . get_current_user_id() );
     223                $recommendation   = ( new FirstPromiseRecommendation() )->recommend(
     224                        $candidates,
     225                        array_map(
     226                                static fn( $product ): array => array(
     227                                        'id'   => (int) $product->get_id(),
     228                                        'name' => trim( (string) $product->get_name() ),
     229                                ),
     230                                $products
     231                        )
     232                );
     233                $activeDefinition = is_array( $active ) ? json_decode( (string) ( $active['definition_json'] ?? '' ), true ) : null;
     234                if ( is_array( $report ) && (int) ( $report['run']['contract_row_id'] ?? 0 ) !== (int) ( $active['id'] ?? 0 ) ) {
     235                        $activeDefinition = null;
     236                }
     237                $verdictPresentation = ( new PromiseVerdictPresentation() )->present(
     238                        is_array( $latest ) ? $latest : null,
     239                        is_array( $activeDefinition ) ? $activeDefinition : null,
     240                        is_array( $report['cases'] ?? null ) ? $report['cases'] : array(),
     241                        is_array( $changeReadiness ) && 'verification_due' === ( $changeReadiness['state'] ?? null )
     242                );
    221243                ?>
    222244                <div class="wrap shipping-contract-guard-wrap">
    … …  
    240262                        </nav>
    241263
    242                         <?php $this->dashboard_panel( $active, $latest, count( $rules ), $changeReadiness, $coverage, $daily_pulse, $incidents ); ?>
     264                        <?php $this->dashboard_panel( $active, $latest, count( $rules ), $changeReadiness, $coverage, $daily_pulse, $incidents, $verdictPresentation ); ?>
    243265
    244266                        <section id="scg-promises" class="scg-panel" aria-labelledby="scg-promises-heading">
    245267                                <h2 id="scg-promises-heading"><?php echo esc_html( is_array( $active ) ? __( 'Your protected shipping promises', 'shipping-contract-guard' ) : __( 'Protect your first shipping promise', 'shipping-contract-guard' ) ); ?></h2>
    246268                                <p><?php echo esc_html( is_array( $active ) ? __( 'These saved expectations are checked together. Open the authoring controls only when you need to add or replace a protected rule.', 'shipping-contract-guard' ) : __( 'Choose one of your existing free-shipping rules. We will check the cart total immediately below, exactly at, and immediately above its threshold.', 'shipping-contract-guard' ) ); ?></p>
    247                                 <?php $this->recommended_setup( $active, $candidates, $products ); ?>
     269                                <?php $this->recommended_setup( $active, $recommendation, $candidates ); ?>
    248270                                <?php if ( $rules ) : ?>
    249271                                        <div class="scg-protected-rules"><h3><?php echo esc_html__( 'Protected rules', 'shipping-contract-guard' ); ?></h3><ul>
    … …  
    256278                                <?php endif; ?>
    257279
    258                                 <details class="scg-advanced-tools scg-promise-authoring" <?php echo is_array( $active ) ? '' : 'open'; ?>>
    259                                         <summary><?php echo esc_html( is_array( $active ) ? __( 'Add or replace a protected rule', 'shipping-contract-guard' ) : __( 'Set up your first protected rule', 'shipping-contract-guard' ) ); ?></summary>
     280                                <details class="scg-advanced-tools scg-promise-authoring">
     281                                        <summary><?php echo esc_html__( 'Customize', 'shipping-contract-guard' ); ?></summary>
    260282                                        <div class="scg-example" role="note">
    261283                                                <strong><?php echo esc_html__( 'Example:', 'shipping-contract-guard' ); ?></strong>
    … …  
    329351                                        <?php endif; ?>
    330352                                </form>
    331                                 </details>
    332353
    333354                                <div class="scg-authored-promise">
    334355                                        <h3><?php echo esc_html__( 'Protect any WooCommerce shipping result', 'shipping-contract-guard' ); ?></h3>
    335356                                        <p><?php echo esc_html__( 'No supported threshold? Define one exact synthetic cart and destination. Shipping Contract Guard records the method identities and prices returned by WooCommerce, then asks you to approve them as the promise to protect.', 'shipping-contract-guard' ); ?></p>
    336                                         <details <?php echo ! $candidates ? 'open' : ''; ?>>
     357                                        <details>
    337358                                                <summary><?php echo esc_html__( 'Create a provider-agnostic shipping promise', 'shipping-contract-guard' ); ?></summary>
    338359                                                <form class="scg-setup-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    … …  
    395416                                        </details>
    396417                                </div>
     418                                </details>
    397419
    398420                                <?php $this->draft_previews( $contracts ); ?>
    … …  
    436458                                </tbody></table>
    437459                                <p class="description"><?php echo esc_html__( 'Assurance report downloads a human-readable, unsigned summary for review before sharing. It sends nothing. Problem found means WooCommerce differed from your saved expectations. Could not verify means an observation was unsafe, ambiguous, or incomplete; it is never counted as working.', 'shipping-contract-guard' ); ?></p>
    438                                 <?php $this->latest_report( $report ); ?>
     460                                <?php $this->latest_report( $report, $verdictPresentation ); ?>
    439461                        </section>
    440462
    … …  
    467489                        </details>
    468490
    469                         <?php $this->operator_panel( $rules, $runs, $connection, $policy, $service_disclosure, $checkout_intent, $alert_email, $alert_test, $incidents, $maintenance_windows, $activation_receipt ); ?>
     491                        <?php $this->operator_panel( $active, $rules, $runs, $connection, $policy, $service_disclosure, $checkout_intent, $alert_email, $alert_test, $incidents, $maintenance_windows, $activation_receipt ); ?>
    470492
    471493                        <section id="scg-settings" class="scg-panel" aria-labelledby="scg-settings-heading">
    … …  
    632654                        <p class="scg-eyebrow"><?php echo esc_html__( 'Guard before, preserve, and recover', 'shipping-contract-guard' ); ?></p>
    633655                        <h2 id="scg-recovery-heading"><?php echo esc_html__( 'Guarded shipping changes', 'shipping-contract-guard' ); ?></h2>
    634                         <p class="scg-section-lead"><?php echo esc_html__( 'Preview the approved-promise impact of one exact core method change, preserve a passing known-good state, or prepare an expiring continuity path.', 'shipping-contract-guard' ); ?></p>
     656                        <p class="scg-section-lead"><?php echo esc_html__( 'Preview turning one supported WooCommerce shipping option on or off, save a passing setup, or prepare a time-limited recovery plan.', 'shipping-contract-guard' ); ?></p>
    635657                        <p><?php echo esc_html__( 'This cannot edit prices, thresholds, destinations, products, coupons, carrier settings, orders, payments, or deployments. The action is recorded before mutation and rolls back on a blocked, incomplete, or errored verification.', 'shipping-contract-guard' ); ?></p>
    636658
    … …  
    20322054        }
    20332055
    2034         private function latest_report( $report ) {
     2056        private function latest_report( $report, array $presentation ) {
    20352057                if ( ! is_array( $report ) || ! is_array( $report['run'] ?? null ) || ! is_array( $report['cases'] ?? null ) ) {
    20362058                        return;
    20372059                }
    2038                 $run  = $report['run'];
    2039                 $view = $this->outcome_view( $run );
    2040                 $open = 'pass' !== ( $run['outcome'] ?? '' );
     2060                $run      = $report['run'];
     2061                $view     = $this->outcome_view( $run );
     2062                $headline = $this->verdict_headline( $presentation, $view['headline'] );
     2063                $open     = 'pass' !== ( $presentation['state'] ?? '' );
    20412064                ?>
    20422065                <details class="scg-run-report" <?php echo $open ? 'open' : ''; ?>>
    … …  
    20452068                        echo esc_html(
    20462069                                sprintf(
    2047                                 /* translators: %s: latest shipping-check outcome. */
     2070                                        /* translators: %s: scoped latest shipping-check headline. */
    20482071                                        __( 'Latest check details — %s', 'shipping-contract-guard' ),
    2049                                         $view['label']
     2072                                        $headline
    20502073                                )
    20512074                        );
    20522075                        ?>
    20532076                        </summary>
     2077                        <div class="scg-promise-verdict">
     2078                                <h3><?php echo esc_html( $headline ); ?></h3>
     2079                                <p><?php echo esc_html( $this->verdict_consequence( $presentation, $run ) ); ?></p>
     2080                                <?php $scope = $this->verdict_scope( (array) ( $presentation['scope'] ?? array() ) ); ?>
     2081                                <?php
     2082                                if ( '' !== $scope ) :
     2083                                        ?>
     2084                                        <p><strong><?php echo esc_html__( 'Saved example:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $scope ); ?></p><?php endif; ?>
     2085                                <?php
     2086                                if ( is_string( $presentation['next_check'] ?? null ) ) :
     2087                                        ?>
     2088                                        <p class="scg-next-action"><strong><?php echo esc_html__( 'Start here:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $this->diagnosis_label( (string) $presentation['next_check'] ) ); ?></p><?php endif; ?>
     2089                                <?php
     2090                                if ( true === ( $presentation['limits_causal'] ?? false ) ) :
     2091                                        ?>
     2092                                        <p class="description"><?php echo esc_html__( 'This suggestion narrows the investigation. It does not prove what caused the result and it does not change store settings.', 'shipping-contract-guard' ); ?></p><?php endif; ?>
     2093                        </div>
    20542094                        <p><?php echo esc_html__( 'Compare what the saved shipping rule expected with what WooCommerce returned. No checkout, customer, or order was created.', 'shipping-contract-guard' ); ?></p>
    20552095                        <table class="widefat striped"><thead><tr><th scope="col"><?php echo esc_html__( 'Result', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Test case', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Expected', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Observed', 'shipping-contract-guard' ); ?></th></tr></thead><tbody>
    … …  
    21132153
    21142154        private function report_case_context( array $case ) {
    2115                 $context     = is_array( $case['local_context'] ?? null ) ? $case['local_context'] : array();
    2116                 $currency    = (string) ( $context['currency'] ?? '' );
    2117                 $amount      = '' !== $currency
     2155                $context    = is_array( $case['local_context'] ?? null ) ? $case['local_context'] : array();
     2156                $has_amount = array_key_exists( 'amount_minor', $context );
     2157                $currency   = trim( (string) ( $context['currency'] ?? '' ) );
     2158                if ( $has_amount && '' === $currency ) {
     2159                        $currency = (string) get_woocommerce_currency();
     2160                }
     2161                $amount      = $has_amount && '' !== $currency
    21182162                        ? wc_price( (int) ( $context['amount_minor'] ?? 0 ) / ( 10 ** wc_get_price_decimals() ), array( 'currency' => $currency ) )
    21192163                        : esc_html__( 'Unknown cart total', 'shipping-contract-guard' );
    … …  
    21322176
    21332177        private function report_expected_methods( array $case ) {
    2134                 $contract = is_array( $case['expected_contract'] ?? null ) ? $case['expected_contract'] : array();
    2135                 $labels   = array();
     2178                $contract        = is_array( $case['expected_contract'] ?? null ) ? $case['expected_contract'] : array();
     2179                $requiredLabels  = array();
     2180                $requiredMethods = array();
     2181                $absentLabels    = array();
    21362182                foreach ( (array) ( $contract['method_expectations'] ?? array() ) as $expectation ) {
    21372183                        if ( ! is_array( $expectation ) ) {
    … …  
    21402186                        $label = $this->method_label( (string) ( $expectation['method_id'] ?? '' ) );
    21412187                        if ( true !== ( $expectation['expected'] ?? false ) ) {
    2142                                 $labels[] = sprintf(
     2188                                $absentLabels[ $label ] = true;
     2189                                continue;
     2190                        }
     2191                        $minimum                   = is_array( $expectation['minimum'] ?? null ) ? $expectation['minimum'] : null;
     2192                        $maximum                   = is_array( $expectation['maximum'] ?? null ) ? $expectation['maximum'] : null;
     2193                        $requiredMethods[ $label ] = true;
     2194                        $label                    .= $this->price_contract_label( $minimum, $maximum, is_array( $expectation['relative'] ?? null ) ? $expectation['relative'] : null );
     2195                        $requiredLabels[]          = $label;
     2196                }
     2197                if ( ! $requiredLabels ) {
     2198                        $result = __( 'No shipping method must be offered', 'shipping-contract-guard' );
     2199                } else {
     2200                        $absent = array_map(
     2201                                static fn( string $label ): string => sprintf(
    21432202                                        /* translators: %s: shipping-method label which must not be available. */
    21442203                                        __( 'No %s', 'shipping-contract-guard' ),
    21452204                                        $label
    2146                                 );
    2147                                 continue;
    2148                         }
    2149                         $minimum  = is_array( $expectation['minimum'] ?? null ) ? $expectation['minimum'] : null;
    2150                         $maximum  = is_array( $expectation['maximum'] ?? null ) ? $expectation['maximum'] : null;
    2151                         $label   .= $this->price_contract_label( $minimum, $maximum, is_array( $expectation['relative'] ?? null ) ? $expectation['relative'] : null );
    2152                         $labels[] = $label;
    2153                 }
    2154                 $result  = $labels ? implode( ', ', $labels ) : __( 'No shipping method must be offered', 'shipping-contract-guard' );
     2205                                ),
     2206                                array_keys( array_diff_key( $absentLabels, $requiredMethods ) )
     2207                        );
     2208                        $result = implode( ', ', array_merge( $requiredLabels, $absent ) );
     2209                }
    21552210                $quality = is_array( $contract['quality'] ?? null ) ? $contract['quality'] : null;
    21562211                if ( is_array( $quality ) ) {
    … …  
    22812336        }
    22822337
    2283         private function dashboard_panel( $active, $latest, $rule_count, $readiness, $coverage, $dailyPulse, array $incidents ) {
     2338        private function dashboard_panel( $active, $latest, $rule_count, $readiness, $coverage, $dailyPulse, array $incidents, array $presentation ) {
     2339                $latestState     = is_array( $latest ) ? (string) ( $presentation['state'] ?? $latest['outcome'] ?? '' ) : '';
     2340                $latestAttention = in_array( $latestState, array( 'fail', 'unknown' ), true );
    22842341                if ( ! is_array( $active ) ) {
    22852342                        $key   = 'setup';
    … …  
    22952352                        );
    22962353                } else {
    2297                         $view     = $this->outcome_view( $latest );
    2298                         $key      = $view['key'];
    2299                         $title    = $view['headline'];
    2300                                 $text = sprintf(
     2354                        $view  = $this->outcome_view( $latest );
     2355                        $key   = 'stale' === ( $presentation['state'] ?? '' ) ? 'unknown' : $view['key'];
     2356                        $title = $this->verdict_headline( $presentation, $view['headline'] );
     2357                        $text  = true === ( $presentation['named'] ?? false ) ? $this->verdict_consequence( $presentation, $latest ) : sprintf(
    23012358                                        /* translators: 1: UTC check timestamp, 2: passing count, 3: failing count, 4: incomplete count. */
    2302                                         __( 'Last checked %1$s UTC: %2$d working, %3$d problems, %4$d could not be verified.', 'shipping-contract-guard' ),
    2303                                         (string) ( $latest['started_at'] ?? '' ),
    2304                                         (int) $latest['pass_count'],
    2305                                         (int) $latest['fail_count'],
    2306                                         (int) $latest['unknown_count']
    2307                                 );
     2359                                __( 'Last checked %1$s UTC: %2$d working, %3$d problems, %4$d could not be verified.', 'shipping-contract-guard' ),
     2360                                (string) ( $latest['started_at'] ?? '' ),
     2361                                (int) $latest['pass_count'],
     2362                                (int) $latest['fail_count'],
     2363                                (int) $latest['unknown_count']
     2364                        );
    23082365                }
    23092366                $nextAction = 'none';
    23102367                if ( is_array( $readiness ) ) {
    2311                         $nextAction = (string) ( $readiness['recommended_action'] ?? 'none' );
    2312                         if ( 'verification_due' === ( $readiness['state'] ?? '' ) ) {
     2368                        $nextAction = $latestAttention ? 'review_latest_check' : (string) ( $readiness['recommended_action'] ?? 'none' );
     2369                        if ( ! $latestAttention && 'verification_due' === ( $readiness['state'] ?? '' ) ) {
    23132370                                $key   = 'unknown';
    23142371                                $title = __( 'A shipping check is due', 'shipping-contract-guard' );
    23152372                                $text  = __( 'A relevant store change was observed after the last verification. Run the same approved promises to obtain a fresh verdict.', 'shipping-contract-guard' );
    2316                         } elseif ( 'coverage_attention' === ( $readiness['state'] ?? '' ) ) {
     2373                        } elseif ( ! $latestAttention && 'coverage_attention' === ( $readiness['state'] ?? '' ) ) {
    23172374                                $key   = 'unknown';
    23182375                                $title = __( 'Shipping coverage needs review', 'shipping-contract-guard' );
    … …  
    23242381                $localTimestamp  = is_array( $latest ) ? strtotime( (string) ( $latest['started_at'] ?? '' ) . ' UTC' ) : false;
    23252382                $changeTimestamp = is_array( $readiness ) ? (int) ( $readiness['last_seen'] ?? 0 ) : 0;
    2326                 $localAttention  = is_array( $readiness ) && in_array( (string) ( $readiness['state'] ?? '' ), array( 'verification_due', 'coverage_attention', 'problem', 'could_not_verify' ), true );
     2383                $localAttention  = $latestAttention || ( is_array( $readiness ) && in_array( (string) ( $readiness['state'] ?? '' ), array( 'verification_due', 'coverage_attention', 'problem', 'could_not_verify' ), true ) );
    23272384                $localIsNewer    = $localAttention && false !== $pulseTimestamp && ( ( false !== $localTimestamp && $localTimestamp >= $pulseTimestamp ) || $changeTimestamp >= $pulseTimestamp );
    23282385                $useDailyPulse   = is_array( $dailyPulse ) && ( ! $localIsNewer || in_array( (string) ( $dailyPulse['state'] ?? '' ), array( 'at_risk', 'could_not_verify' ), true ) );
    … …  
    24572514                        <h2 id="scg-current-status"><?php echo esc_html( $title ); ?></h2>
    24582515                        <p><?php echo esc_html( $text ); ?></p>
    2459                         <ul class="scg-dashboard-facts"><li><strong><?php echo esc_html( (string) $protectedCases ); ?></strong> <?php echo esc_html__( 'approved checks', 'shipping-contract-guard' ); ?></li><li><strong><?php echo esc_html( (string) $rule_count ); ?></strong> <?php echo esc_html__( 'protected promises', 'shipping-contract-guard' ); ?></li><li><strong><?php echo esc_html( $lastEvidence ); ?></strong> <?php echo esc_html( $evidenceLabel ); ?></li></ul>
     2516                        <ul class="scg-dashboard-facts"><li><strong><?php echo esc_html( (string) $protectedCases ); ?></strong> <?php echo esc_html( _n( 'approved check', 'approved checks', $protectedCases, 'shipping-contract-guard' ) ); ?></li><li><strong><?php echo esc_html( (string) $rule_count ); ?></strong> <?php echo esc_html( _n( 'protected promise', 'protected promises', $rule_count, 'shipping-contract-guard' ) ); ?></li><li><strong><?php echo esc_html( $lastEvidence ); ?></strong> <?php echo esc_html( $evidenceLabel ); ?></li></ul>
    24602517                        <p class="scg-next-action"><strong><?php echo esc_html__( 'Next action:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $nextActionText ); ?></p>
    24612518                        <?php if ( is_array( $active ) && 'review_latest_check' === $nextAction ) : ?>
    … …  
    25162573        }
    25172574
     2575        /** Prefer a merchant-written promise name; keep opaque case IDs in technical details. */
     2576        private function operator_promise_label( $active, string $caseId ): string {
     2577                $names = array();
     2578                if ( is_array( $active ) ) {
     2579                        $definition = json_decode( (string) ( $active['definition_json'] ?? '' ), true );
     2580                        foreach ( (array) ( $definition['cases'] ?? array() ) as $case ) {
     2581                                if ( ! is_array( $case ) ) {
     2582                                        continue;
     2583                                }
     2584                                $boundary = is_array( $case['boundary'] ?? null ) ? $case['boundary'] : array();
     2585                                $name     = trim( (string) ( $boundary['dimensions']['rule_name'] ?? '' ) );
     2586                                if ( '' !== $name ) {
     2587                                        $names[ $name ] = true;
     2588                                }
     2589                                if ( '' !== $caseId && hash_equals( (string) ( $boundary['case_id'] ?? '' ), $caseId ) && '' !== $name ) {
     2590                                        return $name;
     2591                                }
     2592                        }
     2593                }
     2594                if ( 1 === count( $names ) ) {
     2595                        return (string) array_key_first( $names );
     2596                }
     2597                return __( 'Protected shipping promise', 'shipping-contract-guard' );
     2598        }
     2599
    25182600        /** Offer the shortest safe path through the existing preview and explicit approval flow. */
    2519         private function recommended_setup( $active, array $candidates, array $products ): void {
    2520                 if ( is_array( $active ) || array() === $products ) {
     2601        private function recommended_setup( $active, array $recommendation, array $candidates ): void {
     2602                if ( is_array( $active ) ) {
     2603                        return;
     2604                }
     2605                if ( true !== ( $recommendation['available'] ?? false ) ) {
     2606                        $reasons = array(
     2607                                'no_supported_amount_rule' => __( 'No enabled WooCommerce Free Shipping rule with a simple minimum order amount is available.', 'shipping-contract-guard' ),
     2608                                'no_eligible_product'      => __( 'No published physical product is available for a safe example cart.', 'shipping-contract-guard' ),
     2609                                'postcode_required'        => __( 'The available Free Shipping rule needs a postcode. Shipping Contract Guard will not guess one.', 'shipping-contract-guard' ),
     2610                                'country_not_explicit'     => __( 'The available Free Shipping rule does not name a country that can be used without guessing.', 'shipping-contract-guard' ),
     2611                        );
     2612                        ?>
     2613                        <div class="scg-recommendation scg-recommendation-unavailable" role="note">
     2614                                <h3><?php echo esc_html__( 'Choose the example yourself', 'shipping-contract-guard' ); ?></h3>
     2615                                <p><?php echo esc_html( $reasons[ (string) ( $recommendation['reason'] ?? '' ) ] ?? $reasons['no_supported_amount_rule'] ); ?></p>
     2616                                <p><?php echo esc_html__( 'Open Customize to choose an invented destination and product, or to save the shipping methods and prices WooCommerce returns for one example cart.', 'shipping-contract-guard' ); ?></p>
     2617                        </div>
     2618                        <?php
    25212619                        return;
    25222620                }
    25232621                $recommended = null;
    25242622                foreach ( $candidates as $candidate ) {
    2525                         $hasPostcode = false;
    2526                         foreach ( (array) ( $candidate['zone_locations'] ?? array() ) as $location ) {
    2527                                 if ( is_array( $location ) && 'postcode' === ( $location['type'] ?? null ) ) {
    2528                                         $hasPostcode = true;
    2529                                         break;
    2530                                 }
    2531                         }
    2532                         if ( ! $hasPostcode ) {
     2623                        if ( is_array( $candidate ) && hash_equals( (string) ( $recommendation['candidate_id'] ?? '' ), (string) ( $candidate['candidate_id'] ?? '' ) ) ) {
    25332624                                $recommended = $candidate;
    25342625                                break;
    … …  
    25382629                        return;
    25392630                }
    2540                 $product  = $products[0];
    2541                 $ruleName = substr( $this->candidate_label( $recommended ), 0, 100 );
    2542                 $country  = $this->base_country();
    2543                 foreach ( (array) ( $recommended['zone_locations'] ?? array() ) as $location ) {
    2544                         if ( is_array( $location ) && 'country' === ( $location['type'] ?? null ) && 1 === preg_match( '/^[A-Z]{2}$/', (string) ( $location['code'] ?? '' ) ) ) {
    2545                                 $country = (string) $location['code'];
    2546                                 break;
    2547                         }
    2548                 }
     2631                $ruleName    = substr( $this->candidate_label( $recommended ), 0, 100 );
     2632                $country     = (string) $recommendation['country'];
     2633                $productId   = (int) $recommendation['product_id'];
     2634                $productName = (string) $recommendation['product_name'];
     2635                $threshold   = wp_strip_all_tags( wc_price( (int) $recommendation['threshold_minor'] / ( 10 ** wc_get_price_decimals() ), array( 'currency' => (string) $recommendation['currency'] ) ) );
    25492636                ?>
    25502637                <div class="scg-recommendation" role="note">
    2551                         <p class="scg-eyebrow"><?php echo esc_html__( 'Recommended first protection', 'shipping-contract-guard' ); ?></p>
    2552                         <h3><?php echo esc_html( $ruleName ); ?></h3>
    2553                         <p><?php echo esc_html__( 'Start with one detected commercial boundary and one shippable product. The next screen still shows every expected method and price for explicit approval.', 'shipping-contract-guard' ); ?></p>
     2638                        <p class="scg-eyebrow"><?php echo esc_html__( 'Recommended first promise', 'shipping-contract-guard' ); ?></p>
     2639                        <h3><?php /* translators: 1: formatted free-shipping threshold, 2: country name, 3: example product name. */ echo esc_html( sprintf( __( 'Protect free shipping from %1$s in %2$s using %3$s.', 'shipping-contract-guard' ), $threshold, $this->country_name( $country ), $productName ) ); ?></h3>
     2640                        <p><?php echo esc_html__( 'Before anything is saved, the preview will show the shipping methods and prices WooCommerce returns for this invented example.', 'shipping-contract-guard' ); ?></p>
     2641                        <ul class="scg-boundary-summary" aria-label="<?php echo esc_attr__( 'Cart totals that will be checked', 'shipping-contract-guard' ); ?>">
     2642                        <?php foreach ( (array) $recommendation['boundary_amounts'] as $index => $minor ) : ?>
     2643                                <li><span><?php echo esc_html( array( __( 'Below', 'shipping-contract-guard' ), __( 'At', 'shipping-contract-guard' ), __( 'Above', 'shipping-contract-guard' ) )[ $index ] ?? __( 'Amount', 'shipping-contract-guard' ) ); ?></span><strong><?php echo esc_html( wp_strip_all_tags( wc_price( (int) $minor / ( 10 ** wc_get_price_decimals() ), array( 'currency' => (string) $recommendation['currency'] ) ) ) ); ?></strong></li>
     2644                        <?php endforeach; ?>
     2645                        </ul>
    25542646                        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    25552647                                <input type="hidden" name="action" value="shipping_contract_guard_create_draft">
    … …  
    25582650                                <input type="hidden" name="criticality" value="release_blocking">
    25592651                                <input type="hidden" name="threshold_candidate" value="<?php echo esc_attr( (string) $recommended['candidate_id'] ); ?>">
    2560                                 <input type="hidden" name="product_id" value="<?php echo esc_attr( (string) $product->get_id() ); ?>">
     2652                                <input type="hidden" name="product_id" value="<?php echo esc_attr( (string) $productId ); ?>">
    25612653                                <input type="hidden" name="country" value="<?php echo esc_attr( $country ); ?>">
    25622654                                <input type="hidden" name="postcode_positive" value="">
    25632655                                <input type="hidden" name="postcode_negative" value="">
    25642656                                <?php wp_nonce_field( 'shipping_contract_guard_create_draft' ); ?>
    2565                                 <?php submit_button( __( 'Preview recommended checks', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?>
     2657                                <?php submit_button( __( 'Preview this promise', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?>
    25662658                        </form>
    25672659                </div>
    … …  
    25742666                        $definition = json_decode( (string) $contract['definition_json'], true );
    25752667                        $cases      = is_array( $definition ) && is_array( $definition['cases'] ?? null ) ? $definition['cases'] : array();
    2576                         $heading    = sprintf(
    2577                                 /* translators: %d: generated shipping-check count. */
    2578                                 __( 'Review %d generated checks', 'shipping-contract-guard' ),
    2579                                 count( $cases )
    2580                         );
     2668                        $ruleNames  = array_values( array_unique( array_map( fn( array $case ): string => $this->case_rule_name( $case ), $cases ) ) );
     2669                        $cartTotals = array_values( array_unique( array_map( fn( array $case ): string => wp_strip_all_tags( $this->case_amount( $case ) ), $cases ) ) );
     2670                        $firstCase  = $cases[0] ?? null;
     2671                        $heading    = 1 === count( $ruleNames )
     2672                                ? sprintf(
     2673                                        /* translators: %s: merchant-named shipping promise. */
     2674                                        __( 'Protect %s', 'shipping-contract-guard' ),
     2675                                        $ruleNames[0]
     2676                                )
     2677                                : __( 'Protect these shipping promises', 'shipping-contract-guard' );
    25812678                        ?>
    25822679                        <div class="scg-preview">
    25832680                                <p class="scg-eyebrow"><?php echo esc_html__( 'Preview', 'shipping-contract-guard' ); ?></p>
    2584                                         <h3><?php echo esc_html( $heading ); ?></h3>
    2585                                 <p><?php echo esc_html__( 'These expectations come from your store’s current behavior. Check them before saving; later checks compare WooCommerce against this version.', 'shipping-contract-guard' ); ?></p>
     2681                                <h3><?php echo esc_html( $heading ); ?></h3>
     2682                                <p><?php echo esc_html__( 'Review what WooCommerce returned for this invented example. Nothing becomes protected until you approve it below.', 'shipping-contract-guard' ); ?></p>
     2683                                <?php if ( is_array( $firstCase ) ) : ?>
     2684                                <ul class="scg-preview-summary">
     2685                                        <li><strong><?php echo esc_html__( 'Example cart:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $this->case_cart( $firstCase ) ); ?></li>
     2686                                        <li><strong><?php echo esc_html__( 'Invented destination:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $this->case_destination( $firstCase ) ); ?></li>
     2687                                        <li><strong><?php echo esc_html__( 'Cart totals:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( implode( ', ', $cartTotals ) ); ?></li>
     2688                                        <?php
     2689                                        if ( count( $ruleNames ) > 1 ) :
     2690                                                ?>
     2691                                                <li><strong><?php echo esc_html__( 'Promises:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( implode( ', ', $ruleNames ) ); ?></li><?php endif; ?>
     2692                                </ul>
     2693                                <?php endif; ?>
     2694                                <p class="description"><?php /* translators: %d: exact supporting check count. */ echo esc_html( sprintf( _n( '%d saved check supports this preview.', '%d saved checks support this preview.', count( $cases ), 'shipping-contract-guard' ), count( $cases ) ) ); ?></p>
    25862695                                <table class="widefat striped"><thead><tr><th scope="col"><?php echo esc_html__( 'Protected rule', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Cart', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Cart total', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Test destination', 'shipping-contract-guard' ); ?></th><th scope="col"><?php echo esc_html__( 'Expected at checkout', 'shipping-contract-guard' ); ?></th></tr></thead><tbody>
    25872696                                <?php foreach ( $cases as $case ) : ?>
    … …  
    25892698                                <?php endforeach; ?>
    25902699                                </tbody></table>
    2591                                 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"><input type="hidden" name="action" value="shipping_contract_guard_approve_and_run_contract"><input type="hidden" name="contract_id" value="<?php echo esc_attr( (string) $contract['id'] ); ?>"><?php wp_nonce_field( 'shipping_contract_guard_approve_and_run_contract' ); ?><?php submit_button( __( 'Save and run first check', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?></form>
     2700                                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"><input type="hidden" name="action" value="shipping_contract_guard_approve_and_run_contract"><input type="hidden" name="contract_id" value="<?php echo esc_attr( (string) $contract['id'] ); ?>"><?php wp_nonce_field( 'shipping_contract_guard_approve_and_run_contract' ); ?><?php submit_button( __( 'Protect and run first check', 'shipping-contract-guard' ), 'primary', 'submit', false ); ?></form>
    25922701                                <details><summary><?php echo esc_html__( 'Technical definition', 'shipping-contract-guard' ); ?></summary><pre class="scg-technical-definition"><?php echo esc_html( wp_json_encode( $definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); ?></pre></details>
    25932702                        </div>
    … …  
    25962705        }
    25972706
    2598         private function operator_panel( array $rules, array $runs, $connection, $policy, $service_disclosure, $checkout_intent, string $alert_email, $alertTest, array $incidents, array $maintenanceWindows, $activationReceipt ) {
     2707        private function operator_panel( $active, array $rules, array $runs, $connection, $policy, $service_disclosure, $checkout_intent, string $alert_email, $alertTest, array $incidents, array $maintenanceWindows, $activationReceipt ) {
    25992708                $has_value  = false;
    26002709                $latest_run = null;
    … …  
    27242833                                <?php if ( $paid_active ) : ?>
    27252834                                        <div class="scg-operator-incidents"><h3><?php echo esc_html__( 'Shipping verdicts and incidents', 'shipping-contract-guard' ); ?></h3>
    2726                                         <p><?php echo esc_html__( 'Operator keeps one incident open for each failed or unknown promise until a later passing check records recovery.', 'shipping-contract-guard' ); ?></p>
     2835                                        <p><?php echo esc_html__( 'Operator keeps each incident open until a later check confirms that shipping works again.', 'shipping-contract-guard' ); ?></p>
    27272836                                        <?php
    27282837                                        if ( ! $incidents ) :
    … …  
    27302839                                                <p class="description"><?php echo esc_html__( 'Refresh to load the signed offsite verdict. No incident state has been cached locally yet.', 'shipping-contract-guard' ); ?></p><?php endif; ?>
    27312840                                        <?php foreach ( $incidents as $finding ) : ?>
    2732                                                 <?php $incident = is_array( $finding['incident'] ?? null ) ? $finding['incident'] : null; ?>
    2733                                                 <div id="scg-incident-<?php echo esc_attr( sanitize_html_class( (string) $finding['case_id'] ) ); ?>" class="scg-preview"><p><span class="scg-result scg-result-<?php echo esc_attr( (string) $finding['state'] ); ?>"><?php echo esc_html( $this->operator_state_label( (string) $finding['state'] ) ); ?></span> <strong><?php echo esc_html( __( 'Promise', 'shipping-contract-guard' ) . ' ' . (string) $finding['case_id'] ); ?></strong></p>
     2841                                                <?php
     2842                                                $incident     = is_array( $finding['incident'] ?? null ) ? $finding['incident'] : null;
     2843                                                $caseId       = (string) ( $finding['case_id'] ?? '' );
     2844                                                $recoveryRun  = is_array( $incident ) ? (string) ( $incident['recovery_run_id'] ?? '' ) : '';
     2845                                                $promiseLabel = $this->operator_promise_label( $active, $caseId );
     2846                                                ?>
     2847                                                <div id="scg-incident-<?php echo esc_attr( sanitize_html_class( $caseId ) ); ?>" class="scg-preview"><p><span class="scg-result scg-result-<?php echo esc_attr( (string) $finding['state'] ); ?>"><?php echo esc_html( $this->operator_state_label( (string) $finding['state'] ) ); ?></span> <strong><?php echo esc_html( $promiseLabel ); ?></strong></p>
    27342848                                                <?php if ( is_array( $incident ) && 'open' === ( $incident['status'] ?? '' ) ) : ?>
    27352849                                                        <p><?php echo esc_html( empty( $incident['acknowledged_at'] ) ? __( 'Needs acknowledgement', 'shipping-contract-guard' ) : __( 'Acknowledged', 'shipping-contract-guard' ) ); ?>
    … …  
    27412855                                                        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"><input type="hidden" name="action" value="shipping_contract_guard_update_operator_incident"><input type="hidden" name="contract_id" value="<?php echo esc_attr( (string) $finding['contract_id'] ); ?>"><input type="hidden" name="case_id" value="<?php echo esc_attr( (string) $finding['case_id'] ); ?>"><input type="hidden" name="episode_number" value="<?php echo esc_attr( (string) $incident['episode_number'] ); ?>"><?php wp_nonce_field( 'shipping_contract_guard_update_operator_incident' ); ?><label><input type="checkbox" name="acknowledge" value="yes" <?php checked( ! empty( $incident['acknowledged_at'] ) ); ?>> <?php echo esc_html__( 'Acknowledge this incident', 'shipping-contract-guard' ); ?></label><p><label><?php echo esc_html__( 'Owner label', 'shipping-contract-guard' ); ?> <input type="text" name="owner_label" maxlength="80" value="<?php echo esc_attr( (string) ( $incident['owner_label'] ?? '' ) ); ?>"></label></p><p><label><?php echo esc_html__( 'Handoff note', 'shipping-contract-guard' ); ?><br><textarea name="incident_note" maxlength="500" rows="3" class="large-text"><?php echo esc_textarea( (string) ( $incident['note'] ?? '' ) ); ?></textarea></label></p><p class="description"><?php echo esc_html__( 'Use operational labels only. Do not enter customer data, credentials, or secrets.', 'shipping-contract-guard' ); ?></p><?php submit_button( __( 'Save incident handoff', 'shipping-contract-guard' ), 'secondary', 'submit', false ); ?></form>
    27422856                                                <?php elseif ( is_array( $incident ) && 'recovered' === ( $incident['status'] ?? '' ) ) : ?>
    2743                                                         <p><strong><?php echo esc_html__( 'Recovery verified', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( (string) ( $incident['recovered_at'] ?? '' ) ); ?> · <code><?php echo esc_html( (string) ( $incident['recovery_run_id'] ?? '' ) ); ?></code></p>
    2744                                                 <?php endif; ?></div>
     2857                                                        <p><strong><?php /* translators: %s: human-readable UTC recovery time. */ echo esc_html( sprintf( __( 'Recovery verified on %s.', 'shipping-contract-guard' ), $this->dashboard_evidence_time( (string) ( $incident['recovered_at'] ?? '' ) ) ) ); ?></strong></p>
     2858                                                <?php endif; ?>
     2859                                                <details class="scg-technical-reference"><summary><?php echo esc_html__( 'Technical reference', 'shipping-contract-guard' ); ?></summary><p><?php echo esc_html__( 'Case:', 'shipping-contract-guard' ); ?> <code><?php echo esc_html( $caseId ); ?></code>
     2860                                                <?php
     2861                                                if ( '' !== $recoveryRun ) :
     2862                                                        ?>
     2863                                                        <br><?php echo esc_html__( 'Recovery run:', 'shipping-contract-guard' ); ?> <code><?php echo esc_html( $recoveryRun ); ?></code><?php endif; ?></p></details></div>
    27452864                                        <?php endforeach; ?></div>
    27462865                                        <div class="scg-operator-settings"><h3><?php echo esc_html__( 'Monitoring controls', 'shipping-contract-guard' ); ?></h3>
    … …  
    27952914                                <p class="notice notice-warning inline"><?php echo esc_html__( 'Automatic checks are unavailable because the service configuration is incomplete.', 'shipping-contract-guard' ); ?></p>
    27962915                        <?php else : ?>
    2797                                 <div class="scg-upgrade"><p class="scg-eyebrow"><?php echo esc_html__( 'Optional subscription', 'shipping-contract-guard' ); ?></p><h3><?php echo esc_html__( 'Your promises work now. Operator keeps watch when nobody is rerunning them.', 'shipping-contract-guard' ); ?></h3>
    2798                                 <p><?php echo esc_html__( 'Subscribe when shipping changes are frequent, releases happen across a team, or a missed rule needs a named owner and defensible recovery record.', 'shipping-contract-guard' ); ?></p>
     2916                                <div class="scg-upgrade"><p class="scg-eyebrow"><?php echo esc_html__( 'Optional subscription', 'shipping-contract-guard' ); ?></p><h3><?php echo esc_html__( 'You proved this shipping promise now. Operator keeps checking it when nobody is watching.', 'shipping-contract-guard' ); ?></h3>
     2917                                <p><?php echo esc_html__( 'Operator reruns your approved checks after relevant changes and on a safe schedule. It alerts one owner and keeps each problem open until a fresh complete check proves recovery.', 'shipping-contract-guard' ); ?></p>
    27992918                                <div class="scg-operator-compare"><div><strong><?php echo esc_html__( 'Local', 'shipping-contract-guard' ); ?></strong><span><?php echo esc_html__( 'You choose when to run, inspect, export, and follow up.', 'shipping-contract-guard' ); ?></span></div><div><strong><?php echo esc_html__( 'Operator', 'shipping-contract-guard' ); ?></strong><span><?php echo esc_html__( 'Relevant change → verdict → alert → owned incident → verified recovery.', 'shipping-contract-guard' ); ?></span></div></div>
    28002919                                <ul class="scg-value-list"><li><?php echo esc_html__( 'Post-change checks coalesce automatically after a quiet period.', 'shipping-contract-guard' ); ?></li><li><?php echo esc_html__( 'A scheduled heartbeat exposes missed local automation and silent drift.', 'shipping-contract-guard' ); ?></li><li><?php echo esc_html__( 'Failures stay grouped and accountable until a passing run proves recovery.', 'shipping-contract-guard' ); ?></li><li><?php echo esc_html__( 'Signed minimized evidence and 90-day offsite history survive local dashboard cleanup.', 'shipping-contract-guard' ); ?></li></ul>
    … …  
    29543073        }
    29553074
     3075        private function verdict_headline( array $presentation, string $fallback ): string {
     3076                if ( true !== ( $presentation['named'] ?? false ) ) {
     3077                        return $fallback;
     3078                }
     3079                $name = (string) $presentation['promise_name'];
     3080                return match ( (string) ( $presentation['state'] ?? '' ) ) {
     3081                        'pass' => sprintf(
     3082                                /* translators: %s: merchant-named protected shipping promise. */
     3083                                __( '%s is working for this saved example.', 'shipping-contract-guard' ),
     3084                                $name
     3085                        ),
     3086                        'fail' => sprintf(
     3087                                /* translators: %s: merchant-named protected shipping promise. */
     3088                                __( '%s may not be working for this saved example.', 'shipping-contract-guard' ),
     3089                                $name
     3090                        ),
     3091                        'unknown' => sprintf(
     3092                                /* translators: %s: merchant-named protected shipping promise. */
     3093                                __( '%s could not be checked safely for this saved example.', 'shipping-contract-guard' ),
     3094                                $name
     3095                        ),
     3096                        'stale' => sprintf(
     3097                                /* translators: %s: merchant-named protected shipping promise. */
     3098                                __( '%s needs a fresh check for this saved example.', 'shipping-contract-guard' ),
     3099                                $name
     3100                        ),
     3101                        default => $fallback,
     3102                };
     3103        }
     3104
     3105        private function verdict_consequence( array $presentation, array $run ): string {
     3106                $checked = (int) ( $run['pass_count'] ?? 0 ) + (int) ( $run['fail_count'] ?? 0 ) + (int) ( $run['unknown_count'] ?? 0 );
     3107                return match ( (string) ( $presentation['state'] ?? '' ) ) {
     3108                        'pass' => sprintf(
     3109                                /* translators: %d: finite number of saved example checks. */
     3110                                _n( 'WooCommerce matched the saved method and price in %d check. This result covers only the saved example.', 'WooCommerce matched the saved methods and prices in all %d checks. This result covers only the saved examples.', $checked, 'shipping-contract-guard' ),
     3111                                $checked
     3112                        ),
     3113                        'fail' => sprintf(
     3114                                /* translators: %d: number of checks that differed from the saved result. */
     3115                                _n( '%d check differed from the saved result. A shopper matching this example may see different shipping.', '%d checks differed from the saved result. Shoppers matching these examples may see different shipping.', (int) ( $run['fail_count'] ?? 0 ), 'shipping-contract-guard' ),
     3116                                (int) ( $run['fail_count'] ?? 0 )
     3117                        ),
     3118                        'unknown' => sprintf(
     3119                                /* translators: %d: number of checks that could not be completed safely. */
     3120                                _n( '%d check could not be completed safely. It is not counted as working.', '%d checks could not be completed safely. They are not counted as working.', (int) ( $run['unknown_count'] ?? 0 ), 'shipping-contract-guard' ),
     3121                                (int) ( $run['unknown_count'] ?? 0 )
     3122                        ),
     3123                        'stale' => __( 'A relevant store change happened after this result. Run the same saved example again before relying on it.', 'shipping-contract-guard' ),
     3124                        default => __( 'Review the saved example and its latest WooCommerce result.', 'shipping-contract-guard' ),
     3125                };
     3126        }
     3127
     3128        private function verdict_scope( array $scope ): string {
     3129                $parts   = array();
     3130                $product = $this->product_name( (int) ( $scope['product_id'] ?? 0 ) );
     3131                if ( '' !== $product ) {
     3132                        $parts[] = $product;
     3133                }
     3134                $country = (string) ( $scope['country'] ?? '' );
     3135                if ( '' !== $country ) {
     3136                        $destination = $this->country_name( $country );
     3137                        if ( '' !== (string) ( $scope['postcode'] ?? '' ) ) {
     3138                                $destination .= ' ' . (string) $scope['postcode'];
     3139                        }
     3140                        $parts[] = $destination;
     3141                }
     3142                $currency = (string) ( $scope['currency'] ?? '' );
     3143                if ( '' !== $currency ) {
     3144                        $parts[] = sprintf(
     3145                                /* translators: %s: formatted invented cart total. */
     3146                                __( 'cart total %s', 'shipping-contract-guard' ),
     3147                                wp_strip_all_tags( wc_price( (int) ( $scope['amount_minor'] ?? 0 ) / ( 10 ** wc_get_price_decimals() ), array( 'currency' => $currency ) ) )
     3148                        );
     3149                }
     3150                return implode( ' · ', $parts );
     3151        }
     3152
    29563153        private function trigger_label( string $trigger ): string {
    29573154                $labels = array(
    … …  
    32833480                }
    32843481        }
     3482
     3483        private function country_name( string $country ): string {
     3484                $country = strtoupper( $country );
     3485                try {
     3486                        $countries = WC()->countries->get_countries();
     3487                        return isset( $countries[ $country ] ) ? (string) $countries[ $country ] : $country;
     3488                } catch ( \Throwable $exception ) {
     3489                        return $country;
     3490                }
     3491        }
    32853492}
  • shipping-contract-guard/trunk/src/Application/ThresholdCandidateCompiler.php

    r3649397 r3650174  
    6565                        }
    6666                }
    67                 throw new ApplicationException( 'The selected threshold is no longer present in the current WooCommerce configuration.' );
     67                throw new ApplicationException( 'The selected Free Shipping rule is no longer available in the current WooCommerce settings.' );
    6868        }
    6969
Note: See TracChangeset for help on using the changeset viewer.