Changeset 3650174
- Timestamp:
- 08/16/2026 08:31:15 PM (2 months ago)
- Location:
- shipping-contract-guard
- Files:
-
- 4 added
- 16 edited
- 1 copied
-
assets/screenshot-1.png (modified) ( previous)
-
assets/screenshot-2.png (modified) ( previous)
-
assets/screenshot-3.png (modified) ( previous)
-
assets/screenshot-4.png (modified) ( previous)
-
tags/1.9.0 (copied) (copied from shipping-contract-guard/trunk )
-
tags/1.9.0/readme.txt (modified) (9 diffs)
-
tags/1.9.0/shipping-contract-guard.php (modified) (2 diffs)
-
tags/1.9.0/src/Admin/Actions.php (modified) (1 diff)
-
tags/1.9.0/src/Admin/Assets.php (modified) (1 diff)
-
tags/1.9.0/src/Admin/Page.php (modified) (30 diffs)
-
tags/1.9.0/src/Application/FirstPromiseRecommendation.php (added)
-
tags/1.9.0/src/Application/PromiseVerdictPresentation.php (added)
-
tags/1.9.0/src/Application/ThresholdCandidateCompiler.php (modified) (1 diff)
-
trunk/readme.txt (modified) (9 diffs)
-
trunk/shipping-contract-guard.php (modified) (2 diffs)
-
trunk/src/Admin/Actions.php (modified) (1 diff)
-
trunk/src/Admin/Assets.php (modified) (1 diff)
-
trunk/src/Admin/Page.php (modified) (30 diffs)
-
trunk/src/Application/FirstPromiseRecommendation.php (added)
-
trunk/src/Application/PromiseVerdictPresentation.php (added)
-
trunk/src/Application/ThresholdCandidateCompiler.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
shipping-contract-guard/tags/1.9.0/readme.txt
r3649397 r3650174 4 4 Tested up to: 7.0 5 5 Requires PHP: 8.1 6 Stable tag: 1. 8.16 Stable tag: 1.9.0 7 7 License: GPLv2 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 12 12 == Description == 13 13 14 Shipping Contract Guard turns important WooCommerce shipping results into repeatable, manager-approved checks.14 Shipping Contract Guard checks whether the shipping options and prices your store promises still work. 15 15 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. 16 Start 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 18 The 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. 17 19 18 20 Local mode is free and requires no account: 19 21 20 22 * 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; 22 24 * run invented carts without creating a checkout, customer, or order; 23 25 * 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; and26 * 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; and28 * activate a n 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. 29 31 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.32 Operator 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. 31 33 32 34 = Deliberate limits = 33 35 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 configurationchange caused a failure.36 Coverage 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. 35 37 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 requestfor the invented cart and destination.38 Core 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. 37 39 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.40 The 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. 39 41 40 42 == Installation == … … 44 46 3. Open **WooCommerce > Shipping Contract Guard**. 45 47 4. 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.48 5. Review the invented carts, expected shipping options, and prices. 47 49 6. Choose **Save and run first check**. 48 50 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.51 Planning, coverage, recovery, and agency tools stay inside the Advanced sections. You do not need them for the first check. 50 52 51 53 Deployment 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. … … 53 55 == Screenshots == 54 56 55 1. Protect one detected shipping boundary with a recommended firstcheck.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.57 1. See a named shipping problem, its shopper impact, and the next thing to check. 58 2. Review the recommended totals and expected shipping result before saving anything. 59 3. Give an Operator incident a clear owner while technical references stay out of the way. 60 4. Confirm that the same protected shipping promise passes again after recovery. 59 61 == External services == 60 62 … … 65 67 = Optional Shipping Contract Guard Operator = 66 68 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.69 The 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. 68 70 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.71 After 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. 70 72 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 boundednote. Do not put customer data, credentials, or secrets in those fields.73 Transfers 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. 72 74 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.75 The 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. 74 76 75 77 Privacy policy: https://shippingcontractguard.com/privacy.html … … 81 83 == Privacy == 82 84 83 Local data includes approved contract definitions, synthetic case results, configuration fingerprints and bounded change facts, run timestamps, local operationaldecisions, 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.85 Local 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. 84 86 85 87 The 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. … … 89 91 = Does local verification require an account? = 90 92 91 No. C ontract setup, approval, manual checks, local history, diagnostics, and exportswork without an account or Shipping Contract Guard service request.93 No. 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. 92 94 93 = Does this inspect my shipping plugin? =95 = Does this read private carrier settings? = 94 96 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.97 No. 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. 96 98 97 99 = Does this test checkout or payment? = 98 100 99 No. It calculates isolated synthetic shipping packages without submitting checkout or creating anorder. Stripe is contacted only when a manager chooses the optional Operator subscription.101 No. 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. 100 102 101 103 = What does Could not verify mean? = … … 109 111 = Are Cart and Checkout blocks supported? = 110 112 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.113 Yes. 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. 112 114 113 115 == 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. 114 123 115 124 = 1.8.1 = … … 122 131 * Simplified the primary manager navigation and moved planning, coverage, and guarded-change workflows behind Advanced sections. 123 132 * Added privacy-safe Site Health information, a diagnostic download, opt-in bounded WooCommerce logging, and direct documentation/support links. 124 * Added a n 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. 126 135 127 136 = 1.7.2 = -
shipping-contract-guard/tags/1.9.0/shipping-contract-guard.php
r3649397 r3650174 3 3 * Plugin Name: Shipping Contract Guard for WooCommerce 4 4 * Description: Catch broken WooCommerce shipping rules before customers do. 5 * Version: 1. 8.15 * Version: 1.9.0 6 6 * Requires at least: 6.5 7 7 * Requires PHP: 8.1 … … 22 22 defined( 'ABSPATH' ) || exit; 23 23 24 define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1. 8.1' );24 define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1.9.0' ); 25 25 define( 'SHIPPING_CONTRACT_GUARD_FILE', __FILE__ ); 26 26 define( 'SHIPPING_CONTRACT_GUARD_DIR', plugin_dir_path( __FILE__ ) ); -
shipping-contract-guard/tags/1.9.0/src/Admin/Actions.php
r3649397 r3650174 945 945 $criticality = isset( $request['criticality'] ) ? sanitize_key( $request['criticality'] ) : 'release_blocking'; 946 946 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 selectionis invalid.' );947 throw new \InvalidArgumentException( 'The country, test product, coupon, or selected Free Shipping rule is invalid.' ); 948 948 } 949 949 $primary = $this->shippable_product( $product ); -
shipping-contract-guard/tags/1.9.0/src/Admin/Assets.php
r3649397 r3650174 18 18 wp_add_inline_style( 19 19 '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}}' 21 21 ); 22 22 wp_add_inline_style( -
shipping-contract-guard/tags/1.9.0/src/Admin/Page.php
r3649397 r3650174 3 3 namespace ShippingContractGuard\Admin; 4 4 5 use ShippingContractGuard\Application\FirstPromiseRecommendation; 6 use ShippingContractGuard\Application\PromiseVerdictPresentation; 5 7 use ShippingContractGuard\LocalApplication; 6 8 use ShippingContractGuard\Lifecycle\ConfigurationChangeAutomation; … … 219 221 $notice = get_transient( 'shipping_contract_guard_notice_' . get_current_user_id() ); 220 222 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 ); 221 243 ?> 222 244 <div class="wrap shipping-contract-guard-wrap"> … … 240 262 </nav> 241 263 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 ); ?> 243 265 244 266 <section id="scg-promises" class="scg-panel" aria-labelledby="scg-promises-heading"> 245 267 <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> 246 268 <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 ); ?> 248 270 <?php if ( $rules ) : ?> 249 271 <div class="scg-protected-rules"><h3><?php echo esc_html__( 'Protected rules', 'shipping-contract-guard' ); ?></h3><ul> … … 256 278 <?php endif; ?> 257 279 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> 260 282 <div class="scg-example" role="note"> 261 283 <strong><?php echo esc_html__( 'Example:', 'shipping-contract-guard' ); ?></strong> … … 329 351 <?php endif; ?> 330 352 </form> 331 </details>332 353 333 354 <div class="scg-authored-promise"> 334 355 <h3><?php echo esc_html__( 'Protect any WooCommerce shipping result', 'shipping-contract-guard' ); ?></h3> 335 356 <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> 337 358 <summary><?php echo esc_html__( 'Create a provider-agnostic shipping promise', 'shipping-contract-guard' ); ?></summary> 338 359 <form class="scg-setup-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> … … 395 416 </details> 396 417 </div> 418 </details> 397 419 398 420 <?php $this->draft_previews( $contracts ); ?> … … 436 458 </tbody></table> 437 459 <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 ); ?> 439 461 </section> 440 462 … … 467 489 </details> 468 490 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 ); ?> 470 492 471 493 <section id="scg-settings" class="scg-panel" aria-labelledby="scg-settings-heading"> … … 632 654 <p class="scg-eyebrow"><?php echo esc_html__( 'Guard before, preserve, and recover', 'shipping-contract-guard' ); ?></p> 633 655 <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 t he 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> 635 657 <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> 636 658 … … 2032 2054 } 2033 2055 2034 private function latest_report( $report ) {2056 private function latest_report( $report, array $presentation ) { 2035 2057 if ( ! is_array( $report ) || ! is_array( $report['run'] ?? null ) || ! is_array( $report['cases'] ?? null ) ) { 2036 2058 return; 2037 2059 } 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'] ?? '' ); 2041 2064 ?> 2042 2065 <details class="scg-run-report" <?php echo $open ? 'open' : ''; ?>> … … 2045 2068 echo esc_html( 2046 2069 sprintf( 2047 /* translators: %s: latest shipping-check outcome. */2070 /* translators: %s: scoped latest shipping-check headline. */ 2048 2071 __( 'Latest check details — %s', 'shipping-contract-guard' ), 2049 $ view['label']2072 $headline 2050 2073 ) 2051 2074 ); 2052 2075 ?> 2053 2076 </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> 2054 2094 <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> 2055 2095 <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> … … 2113 2153 2114 2154 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 2118 2162 ? wc_price( (int) ( $context['amount_minor'] ?? 0 ) / ( 10 ** wc_get_price_decimals() ), array( 'currency' => $currency ) ) 2119 2163 : esc_html__( 'Unknown cart total', 'shipping-contract-guard' ); … … 2132 2176 2133 2177 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(); 2136 2182 foreach ( (array) ( $contract['method_expectations'] ?? array() ) as $expectation ) { 2137 2183 if ( ! is_array( $expectation ) ) { … … 2140 2186 $label = $this->method_label( (string) ( $expectation['method_id'] ?? '' ) ); 2141 2187 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( 2143 2202 /* translators: %s: shipping-method label which must not be available. */ 2144 2203 __( 'No %s', 'shipping-contract-guard' ), 2145 2204 $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 } 2155 2210 $quality = is_array( $contract['quality'] ?? null ) ? $contract['quality'] : null; 2156 2211 if ( is_array( $quality ) ) { … … 2281 2336 } 2282 2337 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 ); 2284 2341 if ( ! is_array( $active ) ) { 2285 2342 $key = 'setup'; … … 2295 2352 ); 2296 2353 } 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( 2301 2358 /* 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 ); 2308 2365 } 2309 2366 $nextAction = 'none'; 2310 2367 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'] ?? '' ) ) { 2313 2370 $key = 'unknown'; 2314 2371 $title = __( 'A shipping check is due', 'shipping-contract-guard' ); 2315 2372 $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'] ?? '' ) ) { 2317 2374 $key = 'unknown'; 2318 2375 $title = __( 'Shipping coverage needs review', 'shipping-contract-guard' ); … … 2324 2381 $localTimestamp = is_array( $latest ) ? strtotime( (string) ( $latest['started_at'] ?? '' ) . ' UTC' ) : false; 2325 2382 $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 ) ); 2327 2384 $localIsNewer = $localAttention && false !== $pulseTimestamp && ( ( false !== $localTimestamp && $localTimestamp >= $pulseTimestamp ) || $changeTimestamp >= $pulseTimestamp ); 2328 2385 $useDailyPulse = is_array( $dailyPulse ) && ( ! $localIsNewer || in_array( (string) ( $dailyPulse['state'] ?? '' ), array( 'at_risk', 'could_not_verify' ), true ) ); … … 2457 2514 <h2 id="scg-current-status"><?php echo esc_html( $title ); ?></h2> 2458 2515 <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> 2460 2517 <p class="scg-next-action"><strong><?php echo esc_html__( 'Next action:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $nextActionText ); ?></p> 2461 2518 <?php if ( is_array( $active ) && 'review_latest_check' === $nextAction ) : ?> … … 2516 2573 } 2517 2574 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 2518 2600 /** 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 2521 2619 return; 2522 2620 } 2523 2621 $recommended = null; 2524 2622 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'] ?? '' ) ) ) { 2533 2624 $recommended = $candidate; 2534 2625 break; … … 2538 2629 return; 2539 2630 } 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'] ) ) ); 2549 2636 ?> 2550 2637 <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> 2554 2646 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> 2555 2647 <input type="hidden" name="action" value="shipping_contract_guard_create_draft"> … … 2558 2650 <input type="hidden" name="criticality" value="release_blocking"> 2559 2651 <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 ); ?>"> 2561 2653 <input type="hidden" name="country" value="<?php echo esc_attr( $country ); ?>"> 2562 2654 <input type="hidden" name="postcode_positive" value=""> 2563 2655 <input type="hidden" name="postcode_negative" value=""> 2564 2656 <?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 ); ?> 2566 2658 </form> 2567 2659 </div> … … 2574 2666 $definition = json_decode( (string) $contract['definition_json'], true ); 2575 2667 $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' ); 2581 2678 ?> 2582 2679 <div class="scg-preview"> 2583 2680 <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> 2586 2695 <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> 2587 2696 <?php foreach ( $cases as $case ) : ?> … … 2589 2698 <?php endforeach; ?> 2590 2699 </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( __( ' Saveand 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> 2592 2701 <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> 2593 2702 </div> … … 2596 2705 } 2597 2706 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 ) { 2599 2708 $has_value = false; 2600 2709 $latest_run = null; … … 2724 2833 <?php if ( $paid_active ) : ?> 2725 2834 <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> 2727 2836 <?php 2728 2837 if ( ! $incidents ) : … … 2730 2839 <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; ?> 2731 2840 <?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> 2734 2848 <?php if ( is_array( $incident ) && 'open' === ( $incident['status'] ?? '' ) ) : ?> 2735 2849 <p><?php echo esc_html( empty( $incident['acknowledged_at'] ) ? __( 'Needs acknowledgement', 'shipping-contract-guard' ) : __( 'Acknowledged', 'shipping-contract-guard' ) ); ?> … … 2741 2855 <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> 2742 2856 <?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> 2745 2864 <?php endforeach; ?></div> 2746 2865 <div class="scg-operator-settings"><h3><?php echo esc_html__( 'Monitoring controls', 'shipping-contract-guard' ); ?></h3> … … 2795 2914 <p class="notice notice-warning inline"><?php echo esc_html__( 'Automatic checks are unavailable because the service configuration is incomplete.', 'shipping-contract-guard' ); ?></p> 2796 2915 <?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__( 'You r 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> 2799 2918 <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> 2800 2919 <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> … … 2954 3073 } 2955 3074 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 2956 3153 private function trigger_label( string $trigger ): string { 2957 3154 $labels = array( … … 3283 3480 } 3284 3481 } 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 } 3285 3492 } -
shipping-contract-guard/tags/1.9.0/src/Application/ThresholdCandidateCompiler.php
r3649397 r3650174 65 65 } 66 66 } 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.' ); 68 68 } 69 69 -
shipping-contract-guard/trunk/readme.txt
r3649397 r3650174 4 4 Tested up to: 7.0 5 5 Requires PHP: 8.1 6 Stable tag: 1. 8.16 Stable tag: 1.9.0 7 7 License: GPLv2 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 12 12 == Description == 13 13 14 Shipping Contract Guard turns important WooCommerce shipping results into repeatable, manager-approved checks.14 Shipping Contract Guard checks whether the shipping options and prices your store promises still work. 15 15 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. 16 Start 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 18 The 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. 17 19 18 20 Local mode is free and requires no account: 19 21 20 22 * 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; 22 24 * run invented carts without creating a checkout, customer, or order; 23 25 * 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; and26 * 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; and28 * activate a n 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. 29 31 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.32 Operator 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. 31 33 32 34 = Deliberate limits = 33 35 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 configurationchange caused a failure.36 Coverage 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. 35 37 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 requestfor the invented cart and destination.38 Core 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. 37 39 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.40 The 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. 39 41 40 42 == Installation == … … 44 46 3. Open **WooCommerce > Shipping Contract Guard**. 45 47 4. 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.48 5. Review the invented carts, expected shipping options, and prices. 47 49 6. Choose **Save and run first check**. 48 50 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.51 Planning, coverage, recovery, and agency tools stay inside the Advanced sections. You do not need them for the first check. 50 52 51 53 Deployment 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. … … 53 55 == Screenshots == 54 56 55 1. Protect one detected shipping boundary with a recommended firstcheck.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.57 1. See a named shipping problem, its shopper impact, and the next thing to check. 58 2. Review the recommended totals and expected shipping result before saving anything. 59 3. Give an Operator incident a clear owner while technical references stay out of the way. 60 4. Confirm that the same protected shipping promise passes again after recovery. 59 61 == External services == 60 62 … … 65 67 = Optional Shipping Contract Guard Operator = 66 68 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.69 The 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. 68 70 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.71 After 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. 70 72 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 boundednote. Do not put customer data, credentials, or secrets in those fields.73 Transfers 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. 72 74 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.75 The 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. 74 76 75 77 Privacy policy: https://shippingcontractguard.com/privacy.html … … 81 83 == Privacy == 82 84 83 Local data includes approved contract definitions, synthetic case results, configuration fingerprints and bounded change facts, run timestamps, local operationaldecisions, 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.85 Local 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. 84 86 85 87 The 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. … … 89 91 = Does local verification require an account? = 90 92 91 No. C ontract setup, approval, manual checks, local history, diagnostics, and exportswork without an account or Shipping Contract Guard service request.93 No. 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. 92 94 93 = Does this inspect my shipping plugin? =95 = Does this read private carrier settings? = 94 96 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.97 No. 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. 96 98 97 99 = Does this test checkout or payment? = 98 100 99 No. It calculates isolated synthetic shipping packages without submitting checkout or creating anorder. Stripe is contacted only when a manager chooses the optional Operator subscription.101 No. 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. 100 102 101 103 = What does Could not verify mean? = … … 109 111 = Are Cart and Checkout blocks supported? = 110 112 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.113 Yes. 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. 112 114 113 115 == 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. 114 123 115 124 = 1.8.1 = … … 122 131 * Simplified the primary manager navigation and moved planning, coverage, and guarded-change workflows behind Advanced sections. 123 132 * Added privacy-safe Site Health information, a diagnostic download, opt-in bounded WooCommerce logging, and direct documentation/support links. 124 * Added a n 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. 126 135 127 136 = 1.7.2 = -
shipping-contract-guard/trunk/shipping-contract-guard.php
r3649397 r3650174 3 3 * Plugin Name: Shipping Contract Guard for WooCommerce 4 4 * Description: Catch broken WooCommerce shipping rules before customers do. 5 * Version: 1. 8.15 * Version: 1.9.0 6 6 * Requires at least: 6.5 7 7 * Requires PHP: 8.1 … … 22 22 defined( 'ABSPATH' ) || exit; 23 23 24 define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1. 8.1' );24 define( 'SHIPPING_CONTRACT_GUARD_VERSION', '1.9.0' ); 25 25 define( 'SHIPPING_CONTRACT_GUARD_FILE', __FILE__ ); 26 26 define( 'SHIPPING_CONTRACT_GUARD_DIR', plugin_dir_path( __FILE__ ) ); -
shipping-contract-guard/trunk/src/Admin/Actions.php
r3649397 r3650174 945 945 $criticality = isset( $request['criticality'] ) ? sanitize_key( $request['criticality'] ) : 'release_blocking'; 946 946 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 selectionis invalid.' );947 throw new \InvalidArgumentException( 'The country, test product, coupon, or selected Free Shipping rule is invalid.' ); 948 948 } 949 949 $primary = $this->shippable_product( $product ); -
shipping-contract-guard/trunk/src/Admin/Assets.php
r3649397 r3650174 18 18 wp_add_inline_style( 19 19 '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}}' 21 21 ); 22 22 wp_add_inline_style( -
shipping-contract-guard/trunk/src/Admin/Page.php
r3649397 r3650174 3 3 namespace ShippingContractGuard\Admin; 4 4 5 use ShippingContractGuard\Application\FirstPromiseRecommendation; 6 use ShippingContractGuard\Application\PromiseVerdictPresentation; 5 7 use ShippingContractGuard\LocalApplication; 6 8 use ShippingContractGuard\Lifecycle\ConfigurationChangeAutomation; … … 219 221 $notice = get_transient( 'shipping_contract_guard_notice_' . get_current_user_id() ); 220 222 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 ); 221 243 ?> 222 244 <div class="wrap shipping-contract-guard-wrap"> … … 240 262 </nav> 241 263 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 ); ?> 243 265 244 266 <section id="scg-promises" class="scg-panel" aria-labelledby="scg-promises-heading"> 245 267 <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> 246 268 <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 ); ?> 248 270 <?php if ( $rules ) : ?> 249 271 <div class="scg-protected-rules"><h3><?php echo esc_html__( 'Protected rules', 'shipping-contract-guard' ); ?></h3><ul> … … 256 278 <?php endif; ?> 257 279 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> 260 282 <div class="scg-example" role="note"> 261 283 <strong><?php echo esc_html__( 'Example:', 'shipping-contract-guard' ); ?></strong> … … 329 351 <?php endif; ?> 330 352 </form> 331 </details>332 353 333 354 <div class="scg-authored-promise"> 334 355 <h3><?php echo esc_html__( 'Protect any WooCommerce shipping result', 'shipping-contract-guard' ); ?></h3> 335 356 <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> 337 358 <summary><?php echo esc_html__( 'Create a provider-agnostic shipping promise', 'shipping-contract-guard' ); ?></summary> 338 359 <form class="scg-setup-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> … … 395 416 </details> 396 417 </div> 418 </details> 397 419 398 420 <?php $this->draft_previews( $contracts ); ?> … … 436 458 </tbody></table> 437 459 <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 ); ?> 439 461 </section> 440 462 … … 467 489 </details> 468 490 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 ); ?> 470 492 471 493 <section id="scg-settings" class="scg-panel" aria-labelledby="scg-settings-heading"> … … 632 654 <p class="scg-eyebrow"><?php echo esc_html__( 'Guard before, preserve, and recover', 'shipping-contract-guard' ); ?></p> 633 655 <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 t he 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> 635 657 <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> 636 658 … … 2032 2054 } 2033 2055 2034 private function latest_report( $report ) {2056 private function latest_report( $report, array $presentation ) { 2035 2057 if ( ! is_array( $report ) || ! is_array( $report['run'] ?? null ) || ! is_array( $report['cases'] ?? null ) ) { 2036 2058 return; 2037 2059 } 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'] ?? '' ); 2041 2064 ?> 2042 2065 <details class="scg-run-report" <?php echo $open ? 'open' : ''; ?>> … … 2045 2068 echo esc_html( 2046 2069 sprintf( 2047 /* translators: %s: latest shipping-check outcome. */2070 /* translators: %s: scoped latest shipping-check headline. */ 2048 2071 __( 'Latest check details — %s', 'shipping-contract-guard' ), 2049 $ view['label']2072 $headline 2050 2073 ) 2051 2074 ); 2052 2075 ?> 2053 2076 </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> 2054 2094 <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> 2055 2095 <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> … … 2113 2153 2114 2154 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 2118 2162 ? wc_price( (int) ( $context['amount_minor'] ?? 0 ) / ( 10 ** wc_get_price_decimals() ), array( 'currency' => $currency ) ) 2119 2163 : esc_html__( 'Unknown cart total', 'shipping-contract-guard' ); … … 2132 2176 2133 2177 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(); 2136 2182 foreach ( (array) ( $contract['method_expectations'] ?? array() ) as $expectation ) { 2137 2183 if ( ! is_array( $expectation ) ) { … … 2140 2186 $label = $this->method_label( (string) ( $expectation['method_id'] ?? '' ) ); 2141 2187 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( 2143 2202 /* translators: %s: shipping-method label which must not be available. */ 2144 2203 __( 'No %s', 'shipping-contract-guard' ), 2145 2204 $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 } 2155 2210 $quality = is_array( $contract['quality'] ?? null ) ? $contract['quality'] : null; 2156 2211 if ( is_array( $quality ) ) { … … 2281 2336 } 2282 2337 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 ); 2284 2341 if ( ! is_array( $active ) ) { 2285 2342 $key = 'setup'; … … 2295 2352 ); 2296 2353 } 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( 2301 2358 /* 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 ); 2308 2365 } 2309 2366 $nextAction = 'none'; 2310 2367 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'] ?? '' ) ) { 2313 2370 $key = 'unknown'; 2314 2371 $title = __( 'A shipping check is due', 'shipping-contract-guard' ); 2315 2372 $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'] ?? '' ) ) { 2317 2374 $key = 'unknown'; 2318 2375 $title = __( 'Shipping coverage needs review', 'shipping-contract-guard' ); … … 2324 2381 $localTimestamp = is_array( $latest ) ? strtotime( (string) ( $latest['started_at'] ?? '' ) . ' UTC' ) : false; 2325 2382 $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 ) ); 2327 2384 $localIsNewer = $localAttention && false !== $pulseTimestamp && ( ( false !== $localTimestamp && $localTimestamp >= $pulseTimestamp ) || $changeTimestamp >= $pulseTimestamp ); 2328 2385 $useDailyPulse = is_array( $dailyPulse ) && ( ! $localIsNewer || in_array( (string) ( $dailyPulse['state'] ?? '' ), array( 'at_risk', 'could_not_verify' ), true ) ); … … 2457 2514 <h2 id="scg-current-status"><?php echo esc_html( $title ); ?></h2> 2458 2515 <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> 2460 2517 <p class="scg-next-action"><strong><?php echo esc_html__( 'Next action:', 'shipping-contract-guard' ); ?></strong> <?php echo esc_html( $nextActionText ); ?></p> 2461 2518 <?php if ( is_array( $active ) && 'review_latest_check' === $nextAction ) : ?> … … 2516 2573 } 2517 2574 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 2518 2600 /** 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 2521 2619 return; 2522 2620 } 2523 2621 $recommended = null; 2524 2622 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'] ?? '' ) ) ) { 2533 2624 $recommended = $candidate; 2534 2625 break; … … 2538 2629 return; 2539 2630 } 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'] ) ) ); 2549 2636 ?> 2550 2637 <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> 2554 2646 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> 2555 2647 <input type="hidden" name="action" value="shipping_contract_guard_create_draft"> … … 2558 2650 <input type="hidden" name="criticality" value="release_blocking"> 2559 2651 <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 ); ?>"> 2561 2653 <input type="hidden" name="country" value="<?php echo esc_attr( $country ); ?>"> 2562 2654 <input type="hidden" name="postcode_positive" value=""> 2563 2655 <input type="hidden" name="postcode_negative" value=""> 2564 2656 <?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 ); ?> 2566 2658 </form> 2567 2659 </div> … … 2574 2666 $definition = json_decode( (string) $contract['definition_json'], true ); 2575 2667 $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' ); 2581 2678 ?> 2582 2679 <div class="scg-preview"> 2583 2680 <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> 2586 2695 <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> 2587 2696 <?php foreach ( $cases as $case ) : ?> … … 2589 2698 <?php endforeach; ?> 2590 2699 </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( __( ' Saveand 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> 2592 2701 <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> 2593 2702 </div> … … 2596 2705 } 2597 2706 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 ) { 2599 2708 $has_value = false; 2600 2709 $latest_run = null; … … 2724 2833 <?php if ( $paid_active ) : ?> 2725 2834 <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> 2727 2836 <?php 2728 2837 if ( ! $incidents ) : … … 2730 2839 <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; ?> 2731 2840 <?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> 2734 2848 <?php if ( is_array( $incident ) && 'open' === ( $incident['status'] ?? '' ) ) : ?> 2735 2849 <p><?php echo esc_html( empty( $incident['acknowledged_at'] ) ? __( 'Needs acknowledgement', 'shipping-contract-guard' ) : __( 'Acknowledged', 'shipping-contract-guard' ) ); ?> … … 2741 2855 <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> 2742 2856 <?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> 2745 2864 <?php endforeach; ?></div> 2746 2865 <div class="scg-operator-settings"><h3><?php echo esc_html__( 'Monitoring controls', 'shipping-contract-guard' ); ?></h3> … … 2795 2914 <p class="notice notice-warning inline"><?php echo esc_html__( 'Automatic checks are unavailable because the service configuration is incomplete.', 'shipping-contract-guard' ); ?></p> 2796 2915 <?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__( 'You r 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> 2799 2918 <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> 2800 2919 <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> … … 2954 3073 } 2955 3074 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 2956 3153 private function trigger_label( string $trigger ): string { 2957 3154 $labels = array( … … 3283 3480 } 3284 3481 } 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 } 3285 3492 } -
shipping-contract-guard/trunk/src/Application/ThresholdCandidateCompiler.php
r3649397 r3650174 65 65 } 66 66 } 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.' ); 68 68 } 69 69
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)