@wix/agent-skills 1.33.1 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/skills/wix-export/SKILL.md +83 -0
- package/skills/wix-harmony-editor/.publish-manifest.json +8 -0
- package/skills/wix-harmony-editor/COPY-SITE.md +494 -0
- package/skills/wix-harmony-editor/SKILL.md +317 -0
- package/skills/wix-harmony-editor/scripts/copy/build-description.mjs +249 -0
- package/skills/wix-harmony-editor/scripts/copy/check-description.mjs +225 -0
- package/skills/wix-harmony-editor/scripts/copy/check-mirror.mjs +89 -0
- package/skills/wix-harmony-editor/scripts/copy/compare-preview.mjs +76 -0
- package/skills/wix-harmony-editor/scripts/copy/deploy-description.mjs +212 -0
- package/skills/wix-harmony-editor/scripts/copy/desc-hash.mjs +44 -0
- package/skills/wix-harmony-editor/scripts/copy/editor-run.mjs +45 -0
- package/skills/wix-harmony-editor/scripts/copy/export-page.mjs +53 -0
- package/skills/wix-harmony-editor/scripts/copy/find-interactions.mjs +76 -0
- package/skills/wix-harmony-editor/scripts/copy/hide-scrollbars.mjs +16 -0
- package/skills/wix-harmony-editor/scripts/copy/import-media.mjs +150 -0
- package/skills/wix-harmony-editor/scripts/copy/link-url.mjs +27 -0
- package/skills/wix-harmony-editor/scripts/copy/lint-api.mjs +56 -0
- package/skills/wix-harmony-editor/scripts/copy/measure-source.mjs +211 -0
- package/skills/wix-harmony-editor/scripts/copy/merge-mirrors.mjs +29 -0
- package/skills/wix-harmony-editor/scripts/copy/mirror-site.mjs +92 -0
- package/skills/wix-harmony-editor/scripts/copy/mobile-stage.mjs +56 -0
- package/skills/wix-harmony-editor/scripts/copy/preview-widths.mjs +73 -0
- package/skills/wix-harmony-editor/scripts/copy/render-plate.mjs +67 -0
- package/skills/wix-harmony-editor/scripts/copy/serve-mirror.mjs +102 -0
- package/skills/wix-harmony-editor/scripts/copy/snapshot-capabilities.mjs +40 -0
- package/skills/wix-harmony-editor/scripts/copy/split-sections.mjs +191 -0
- package/skills/wix-harmony-editor/scripts/copy/upload-media-manager.mjs +61 -0
- package/skills/wix-harmony-editor/scripts/launch.sh +359 -0
- package/skills/wix-headless/SKILL.md +7 -2
- package/skills/wix-headless/references/CAPABILITIES.md +6 -1
- package/skills/wix-headless/references/DISCOVERY.md +2 -1
- package/skills/wix-headless/references/SDK_HANDOFF.md +6 -1
- package/skills/wix-headless/references/SEED.md +3 -2
- package/skills/wix-headless/references/SETUP.md +1 -0
- package/skills/wix-headless/references/inline-recipes/experience-donations.md +102 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-donations.md +146 -0
- package/skills/wix-headless/references/inline-recipes/setup-donations.md +15 -0
- package/skills/wix-headless/references/managed/CREATE.md +1 -1
- package/skills/wix-headless-kit/SKILL.md +110 -21
- package/skills/wix-headless-kit/guides/api-run.md +15 -13
- package/skills/wix-headless-kit/guides/existing-site.md +31 -0
- package/skills/wix-headless-kit/guides/migration.md +49 -17
- package/skills/wix-headless-kit/install/agents-md.mjs +1 -1
- package/skills/wix-headless-kit/install/context.mjs +61 -6
- package/skills/wix-headless-kit/install/pins.json +1 -1
- package/skills/wix-headless-kit/install/setup.mjs +107 -12
- package/skills/wix-headless-replatform/.publish-manifest.json +8 -0
- package/skills/wix-headless-replatform/SKILL.md +116 -0
- package/skills/wix-headless-replatform/agents/openai.yaml +6 -0
- package/skills/wix-headless-replatform/references/control-and-visual-fidelity-contract.md +43 -0
- package/skills/wix-headless-replatform/references/extraction-schema.md +187 -0
- package/skills/wix-headless-replatform/references/interaction-runtime-contract.md +137 -0
- package/skills/wix-headless-replatform/references/layout-blueprint-contract.md +41 -0
- package/skills/wix-headless-replatform/references/output-contract.md +93 -0
- package/skills/wix-headless-replatform/references/post-build-gap-contract.md +125 -0
- package/skills/wix-headless-replatform/references/qa-checklist.md +83 -0
- package/skills/wix-headless-replatform/references/safety.md +19 -0
- package/skills/wix-headless-replatform/references/ui-normalization-contract.md +70 -0
- package/skills/wix-headless-replatform/references/workflow.md +157 -0
- package/skills/wix-headless-replatform/registry/README.md +20 -0
- package/skills/wix-headless-replatform/registry/approvals/button/shadcn-default-ac60ef5.json +28 -0
- package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/button.tsx +54 -0
- package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/capabilities.json +60 -0
- package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/contract.md +48 -0
- package/skills/wix-headless-replatform/registry/licenses/shadcn-ui-ac60ef5-MIT.txt +21 -0
- package/skills/wix-headless-replatform/registry/registry.json +36 -0
- package/skills/wix-headless-replatform/resources/rp-facelift/SKILL.md +32 -0
- package/skills/wix-headless-replatform/resources/rp-project-context/SKILL.md +62 -0
- package/skills/wix-headless-replatform/resources/rp-qa-gap-loop/SKILL.md +64 -0
- package/skills/wix-headless-replatform/resources/rp-source-evidence/SKILL.md +42 -0
- package/skills/wix-headless-replatform/resources/rp-ui-implementation/SKILL.md +73 -0
- package/skills/wix-headless-replatform/schemas/build-plan.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/candidate.schema.json +19 -0
- package/skills/wix-headless-replatform/schemas/common.schema.json +33 -0
- package/skills/wix-headless-replatform/schemas/decision-patch.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/decision-request.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/extraction-gap.schema.json +28 -0
- package/skills/wix-headless-replatform/schemas/extraction-manifest.schema.json +34 -0
- package/skills/wix-headless-replatform/schemas/final-report.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/foundation.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/metadata.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/observation-packet.schema.json +17 -0
- package/skills/wix-headless-replatform/schemas/page-capture.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/page-resolution.schema.json +33 -0
- package/skills/wix-headless-replatform/schemas/shared-chrome.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/unit.schema.json +40 -0
- package/skills/wix-headless-replatform/scripts/assemble-extraction.mjs +844 -0
- package/skills/wix-headless-replatform/scripts/benchmark-extraction.mjs +163 -0
- package/skills/wix-headless-replatform/scripts/browser-extraction-preflight.mjs +42 -0
- package/skills/wix-headless-replatform/scripts/build-extracted-homepage.mjs +228 -0
- package/skills/wix-headless-replatform/scripts/clear-wix-starter.mjs +87 -0
- package/skills/wix-headless-replatform/scripts/create-wix-project.mjs +505 -0
- package/skills/wix-headless-replatform/scripts/discover-urls.mjs +142 -0
- package/skills/wix-headless-replatform/scripts/extract-assets.mjs +140 -0
- package/skills/wix-headless-replatform/scripts/extract-design-system.mjs +637 -0
- package/skills/wix-headless-replatform/scripts/extract-interactions.mjs +1903 -0
- package/skills/wix-headless-replatform/scripts/extract-page.mjs +2461 -0
- package/skills/wix-headless-replatform/scripts/extract-seo.mjs +25 -0
- package/skills/wix-headless-replatform/scripts/extract-tokens.mjs +63 -0
- package/skills/wix-headless-replatform/scripts/finalize-extraction-report.mjs +126 -0
- package/skills/wix-headless-replatform/scripts/finalize-gap-review.mjs +180 -0
- package/skills/wix-headless-replatform/scripts/finalize-gap-review.test.mjs +98 -0
- package/skills/wix-headless-replatform/scripts/finalize-migration-website.mjs +103 -0
- package/skills/wix-headless-replatform/scripts/generate-control-state-contract.mjs +176 -0
- package/skills/wix-headless-replatform/scripts/generate-design-md.mjs +261 -0
- package/skills/wix-headless-replatform/scripts/generate-extraction-validators.mjs +25 -0
- package/skills/wix-headless-replatform/scripts/generate-invoice-download-backend.mjs +342 -0
- package/skills/wix-headless-replatform/scripts/generate-layout-blueprint.mjs +140 -0
- package/skills/wix-headless-replatform/scripts/generate-repeater-cms.mjs +22 -0
- package/skills/wix-headless-replatform/scripts/generate-route-plan.mjs +143 -0
- package/skills/wix-headless-replatform/scripts/generate-scene-contract.mjs +288 -0
- package/skills/wix-headless-replatform/scripts/generate-ui-normalization.mjs +266 -0
- package/skills/wix-headless-replatform/scripts/generate-visual-assets.mjs +111 -0
- package/skills/wix-headless-replatform/scripts/lib/browser-tooling.mjs +570 -0
- package/skills/wix-headless-replatform/scripts/lib/common.mjs +329 -0
- package/skills/wix-headless-replatform/scripts/lib/component-capabilities.mjs +121 -0
- package/skills/wix-headless-replatform/scripts/lib/component-registry.mjs +282 -0
- package/skills/wix-headless-replatform/scripts/lib/content-boundary.mjs +61 -0
- package/skills/wix-headless-replatform/scripts/lib/extraction-contract.mjs +374 -0
- package/skills/wix-headless-replatform/scripts/lib/extraction-detectors.mjs +243 -0
- package/skills/wix-headless-replatform/scripts/lib/extraction-validators.generated.mjs +1139 -0
- package/skills/wix-headless-replatform/scripts/lib/font-contract.mjs +326 -0
- package/skills/wix-headless-replatform/scripts/lib/frontend-automation-state.mjs +184 -0
- package/skills/wix-headless-replatform/scripts/lib/gap-analysis.mjs +396 -0
- package/skills/wix-headless-replatform/scripts/lib/html-extract.mjs +168 -0
- package/skills/wix-headless-replatform/scripts/lib/interaction-bootstrap.mjs +10 -0
- package/skills/wix-headless-replatform/scripts/lib/interaction-runtime.mjs +194 -0
- package/skills/wix-headless-replatform/scripts/lib/interaction-timeline.mjs +348 -0
- package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/classifiers.mjs +141 -0
- package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/geometry.mjs +43 -0
- package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/vocabulary.mjs +18 -0
- package/skills/wix-headless-replatform/scripts/lib/ui-normalization.css +64 -0
- package/skills/wix-headless-replatform/scripts/optimize-svg.mjs +31 -0
- package/skills/wix-headless-replatform/scripts/post-build-gap.mjs +358 -0
- package/skills/wix-headless-replatform/scripts/provision-repeater-cms.mjs +25 -0
- package/skills/wix-headless-replatform/scripts/record-gap-decision.mjs +39 -0
- package/skills/wix-headless-replatform/scripts/replay-extraction.mjs +107 -0
- package/skills/wix-headless-replatform/scripts/resolve-home-page.mjs +96 -0
- package/skills/wix-headless-replatform/scripts/site-clone.mjs +658 -0
- package/skills/wix-headless-replatform/scripts/validate-font-contract.mjs +29 -0
- package/skills/wix-headless-replatform/scripts/verify-interactions.mjs +669 -0
- package/skills/wix-headless-replatform/scripts/visual-qa.mjs +370 -0
- package/skills/wix-headless-templates/donations/INSTRUCTIONS.md +37 -24
- package/skills/wix-headless-templates/donations/app/components/donations/DonateForm.tsx +49 -23
- package/skills/wix-headless-templates/donations/app/components/donations/DonationReceipt.tsx +6 -5
- package/skills/wix-headless-templates/donations/app/components/donations/GoalProgress.tsx +5 -5
- package/skills/wix-headless-templates/donations/app/wix/donations/campaigns.ts +4 -4
- package/skills/wix-headless-templates/donations/app/wix/donations/donate.ts +7 -3
- package/skills/wix-headless-templates/donations/app/wix/donations/donation-store.ts +2 -2
- package/skills/wix-headless-templates/donations/app/wix/donations/donations-core.ts +12 -9
- package/skills/wix-headless-templates/donations/project/src/components/donations/DonateForm.tsx +49 -23
- package/skills/wix-headless-templates/donations/project/src/components/donations/DonationReceipt.tsx +6 -5
- package/skills/wix-headless-templates/donations/project/src/components/donations/GoalProgress.tsx +5 -5
- package/skills/wix-headless-templates/donations/project/src/wix/donations/campaigns.ts +4 -4
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donate.ts +7 -3
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donation-store.ts +2 -2
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donations-core.ts +12 -9
- package/skills/wix-headless-templates/donations/rest/campaigns.ts +4 -4
- package/skills/wix-headless-templates/donations/rest/donate.ts +7 -3
- package/skills/wix-headless-templates/donations/seed/SEED.md +12 -7
- package/skills/wix-headless-templates/donations/tests/donation-contract.test.mjs +71 -0
- package/skills/wix-headless-templates/donations/tests/donation-form.test.cjs +137 -0
- package/skills/wix-headless-templates/restaurants/INSTRUCTIONS.md +28 -14
- package/skills/wix-headless-templates/restaurants/app/components/restaurants/OrderCartDrawer.tsx +87 -5
- package/skills/wix-headless-templates/restaurants/app/hooks/restaurants/useOrderCart.ts +9 -1
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/order-store.ts +33 -4
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/ordering-core.ts +100 -5
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/ordering.ts +101 -15
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/types.ts +35 -1
- package/skills/wix-headless-templates/restaurants/project/src/components/restaurants/OrderCartDrawer.tsx +87 -5
- package/skills/wix-headless-templates/restaurants/project/src/hooks/restaurants/useOrderCart.ts +9 -1
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/order-store.ts +33 -4
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/ordering-core.ts +100 -5
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/ordering.ts +101 -15
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/types.ts +35 -1
- package/skills/wix-headless-templates/restaurants/rest/ordering.ts +91 -14
- package/skills/wix-import/SKILL.md +90 -0
- package/skills/wix-manage/SKILL.md +13 -3
- package/skills/wix-manage/references/dashboard-navigation/dashboard-navigation.md +2 -1
- package/skills/wix-manage/references/dashboard-navigation/routes.json +249 -0
- package/skills/wix-manage/references/donations/create-and-update-donation-campaigns.md +139 -0
- package/skills/wix-manage/references/events/events-dashboard-navigation.md +81 -0
- package/skills/wix-manage/references/sites/manage-oauth-apps.md +1 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +127 -8
- package/skills/wix-replatform/.publish-manifest.json +5 -5
- package/skills/wix-replatform/CLI.md +162 -0
- package/skills/wix-replatform/README.md +52 -0
- package/skills/wix-replatform/SKILL.md +229 -684
- package/skills/wix-replatform/console/account.css +1 -0
- package/skills/wix-replatform/console/app.css +1 -0
- package/skills/wix-replatform/console/app.js +117 -0
- package/skills/wix-replatform/console/index.html +4 -0
- package/skills/wix-replatform/console/json.css +11 -0
- package/skills/wix-replatform/lib/agent-adapters/index.js +71 -0
- package/skills/wix-replatform/lib/app-page-routing.js +75 -0
- package/skills/wix-replatform/lib/artifact-freshness.js +11 -1
- package/skills/wix-replatform/lib/blocked-data-requests.js +427 -0
- package/skills/wix-replatform/lib/census-reconciliation.js +426 -0
- package/skills/wix-replatform/lib/completion-report.js +1393 -13
- package/skills/wix-replatform/lib/config-env.js +85 -11
- package/skills/wix-replatform/lib/confirmed-pictures.js +212 -0
- package/skills/wix-replatform/lib/dependency-readiness.js +423 -0
- package/skills/wix-replatform/lib/dependency-recovery.js +21 -0
- package/skills/wix-replatform/lib/destination-choice.js +163 -0
- package/skills/wix-replatform/lib/destination-handoff.js +62 -0
- package/skills/wix-replatform/lib/destination-http.js +81 -0
- package/skills/wix-replatform/lib/destination-route.js +98 -0
- package/skills/wix-replatform/lib/destination.js +919 -0
- package/skills/wix-replatform/lib/discovered-census.js +231 -0
- package/skills/wix-replatform/lib/discovered-set-status.js +255 -0
- package/skills/wix-replatform/lib/document-url-guard.js +203 -0
- package/skills/wix-replatform/lib/entity-qa-attestation.js +339 -0
- package/skills/wix-replatform/lib/entity-verification.js +18 -0
- package/skills/wix-replatform/lib/execution-capability.js +120 -0
- package/skills/wix-replatform/lib/execution-log.js +170 -0
- package/skills/wix-replatform/lib/execution-manifest.js +20 -2
- package/skills/wix-replatform/lib/execution-state.js +22 -0
- package/skills/wix-replatform/lib/expectations-ledger.js +96 -0
- package/skills/wix-replatform/lib/generated-plugin-handoff.js +140 -0
- package/skills/wix-replatform/lib/import-readiness-contract.js +23 -0
- package/skills/wix-replatform/lib/import-recovery.js +800 -6
- package/skills/wix-replatform/lib/imported-redirect-targets.js +608 -0
- package/skills/wix-replatform/lib/internal-link-reconciliation.js +166 -0
- package/skills/wix-replatform/lib/link-audit.js +173 -0
- package/skills/wix-replatform/lib/link-census.js +194 -0
- package/skills/wix-replatform/lib/link-plan.js +148 -0
- package/skills/wix-replatform/lib/link-verification.js +274 -0
- package/skills/wix-replatform/lib/link-write-journal.js +42 -0
- package/skills/wix-replatform/lib/local-state.js +869 -30
- package/skills/wix-replatform/lib/member-completion.js +658 -0
- package/skills/wix-replatform/lib/member-crosswalk.js +69 -0
- package/skills/wix-replatform/lib/member-outcome-registry.js +265 -0
- package/skills/wix-replatform/lib/migration-completion.js +311 -0
- package/skills/wix-replatform/lib/operator-identity.js +47 -0
- package/skills/wix-replatform/lib/orchestration-decisions.js +248 -0
- package/skills/wix-replatform/lib/orchestration-preflight.js +401 -19
- package/skills/wix-replatform/lib/orchestration-router.js +608 -32
- package/skills/wix-replatform/lib/orchestration-state.js +188 -12
- package/skills/wix-replatform/lib/order-reference-readiness.js +30 -0
- package/skills/wix-replatform/lib/payments-outcome-registry.js +200 -0
- package/skills/wix-replatform/lib/plugin-cms-fallback.js +618 -0
- package/skills/wix-replatform/lib/preservation-approval-stamp.js +54 -0
- package/skills/wix-replatform/lib/product-media-deferral.js +96 -0
- package/skills/wix-replatform/lib/progress-log.js +19 -3
- package/skills/wix-replatform/lib/read-back-approval.js +184 -0
- package/skills/wix-replatform/lib/read-back-comparison.js +136 -0
- package/skills/wix-replatform/lib/read-back-contract.js +243 -0
- package/skills/wix-replatform/lib/read-back-extract.js +118 -0
- package/skills/wix-replatform/lib/read-back-gate.js +734 -0
- package/skills/wix-replatform/lib/read-back-reads.js +94 -0
- package/skills/wix-replatform/lib/read-back-state.js +107 -0
- package/skills/wix-replatform/lib/read-back-summary.js +81 -0
- package/skills/wix-replatform/lib/rerun-conflicts.js +334 -0
- package/skills/wix-replatform/lib/run-controller.js +401 -0
- package/skills/wix-replatform/lib/seo-outcome-registry.js +45 -0
- package/skills/wix-replatform/lib/setup-verification.js +148 -0
- package/skills/wix-replatform/lib/source-access.js +231 -0
- package/skills/wix-replatform/lib/source-coverage.js +1130 -0
- package/skills/wix-replatform/lib/source-secrets.js +277 -0
- package/skills/wix-replatform/lib/structured-link-patch.js +59 -0
- package/skills/wix-replatform/lib/task-state.js +110 -0
- package/skills/wix-replatform/lib/url-preservation-report.js +319 -0
- package/skills/wix-replatform/lib/url-preservation-state.js +349 -1
- package/skills/wix-replatform/lib/url-redirect-plan.js +728 -0
- package/skills/wix-replatform/lib/url-verification.js +315 -0
- package/skills/wix-replatform/lib/user-action-effects.js +42 -0
- package/skills/wix-replatform/lib/user-actions.js +264 -0
- package/skills/wix-replatform/lib/website-handoff.js +1220 -0
- package/skills/wix-replatform/lib/wix-client-callers.js +54 -0
- package/skills/wix-replatform/lib/wix-dashboard-url.js +35 -0
- package/skills/wix-replatform/lib/write-verification-state.js +92 -0
- package/skills/wix-replatform/lib/write-verification.js +274 -0
- package/skills/wix-replatform/resources/rp-destination/SKILL.md +137 -0
- package/skills/wix-replatform/resources/rp-discovery/SKILL.md +189 -16
- package/skills/wix-replatform/resources/rp-discovery/scripts/promote-source-coverage.js +47 -0
- package/skills/wix-replatform/resources/rp-execute-import/SKILL.md +869 -29
- package/skills/wix-replatform/resources/rp-execute-import/scripts/shipping-estimate-audit.js +70 -0
- package/skills/wix-replatform/resources/rp-execute-setup/SKILL.md +195 -29
- package/skills/wix-replatform/resources/rp-execute-setup/scripts/execute-setup.js +442 -0
- package/skills/wix-replatform/resources/rp-execution-policy/SKILL.md +112 -0
- package/skills/wix-replatform/resources/rp-execution-policy/lib/coupon-currency-plan-report.js +143 -0
- package/skills/wix-replatform/resources/rp-execution-policy/lib/payments-report.js +244 -0
- package/skills/wix-replatform/resources/rp-import-codegen/SKILL.md +2082 -38
- package/skills/wix-replatform/resources/rp-mapper/SKILL.md +528 -11
- package/skills/wix-replatform/resources/rp-mapper/lib/structure-bridge-planning.js +303 -0
- package/skills/wix-replatform/resources/rp-mapper/scripts/coverage-reconcile.js +88 -0
- package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +6 -5
- package/skills/wix-replatform/resources/rp-preflight/SKILL.md +40 -0
- package/skills/wix-replatform/resources/rp-project-intake/SKILL.md +55 -0
- package/skills/wix-replatform/resources/rp-quick-runtime/SKILL.md +60 -0
- package/skills/wix-replatform/resources/rp-quick-runtime/lib/import-progress.js +89 -0
- package/skills/wix-replatform/resources/rp-quick-runtime/lib/limit-recovery.js +36 -0
- package/skills/wix-replatform/resources/rp-quick-shopify/SKILL.md +123 -0
- package/skills/wix-replatform/resources/rp-quick-shopify/quick-mode.json +124 -0
- package/skills/wix-replatform/resources/rp-quick-shopify/scripts/quick-mode.js +522 -0
- package/skills/wix-replatform/resources/rp-quick-woocommerce/SKILL.md +56 -0
- package/skills/wix-replatform/resources/rp-quick-woocommerce/quick-mode.json +91 -0
- package/skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js +255 -0
- package/skills/wix-replatform/resources/rp-setup-discovery/SKILL.md +80 -10
- package/skills/wix-replatform/resources/rp-setup-discovery/scripts/quick-setup-plan.js +44 -0
- package/skills/wix-replatform/resources/rp-source-csv/SKILL.md +3 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fileset.js +0 -0
- package/skills/wix-replatform/resources/rp-source-csv/scripts/csv-discovery.js +21 -1
- package/skills/wix-replatform/resources/rp-source-inputs/SKILL.md +66 -0
- package/skills/wix-replatform/resources/rp-source-sfcc/SKILL.md +153 -0
- package/skills/wix-replatform/resources/rp-source-shopify/SKILL.md +394 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/discovery-reads.js +200 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/import-findings.js +34 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-bulk-sqlite-worker.js +44 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-bulk.js +1470 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-cli.js +446 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-gift-cards.js +474 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-protected-fields.js +879 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-session.js +426 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/articles.graphql +24 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/blogs.graphql +18 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/collections.graphql +40 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/customers.graphql +76 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/delivery-profiles.graphql +51 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/discounts.graphql +35 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/files.graphql +20 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/gift-cards.graphql +18 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/inventory.graphql +31 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/locations.graphql +21 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/menus.graphql +21 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/metaobjects.graphql +30 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-core.graphql +150 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-fulfillments.graphql +36 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-refunds.graphql +48 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/pages.graphql +20 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-address.graphql +8 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-email.graphql +6 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-name.graphql +8 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-phone.graphql +8 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-address.graphql +12 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-email.graphql +6 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-name.graphql +13 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-phone.graphql +13 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/products.graphql +73 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/url-redirects.graphql +13 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-discovery.js +693 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-extract.js +283 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-gift-card-remedy.js +191 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-session-check.js +115 -0
- package/skills/wix-replatform/resources/rp-source-shopify/source-schema.example.json +201 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/PLUGIN-READ-PLANS.md +146 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/SKILL.md +736 -22
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/blocked-data-handlers.js +73 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/checkout-field-csv-parser.js +115 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/checkout-field-csv-parser.test-fixture.js +91 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/coupon-discovery.js +440 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/currency-gate.js +125 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-source-keys.js +252 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-source-keys.json +183 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-summary.mjs +333 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/invoice-discovery.js +426 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-disposition.js +400 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-entity-read-plan.js +633 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-knowledge.js +1127 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/refund-corroboration.js +194 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/refund-corroboration.json +100 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/sampled-ids-batch.js +157 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/secret-value-guard.js +234 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/store-currency.js +101 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/structure-bridge-reader.js +755 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wix-wp-plugin-v2-client.js +1074 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wix-wp-plugin-v2-client.test-fixture.js +149 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/woo-inventory.js +25 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-http.js +204 -9
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-image-url.js +186 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-plugin-detect.js +1422 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-redirect-sources.js +343 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-route-classifier.js +262 -26
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/yoast-product-seo-evidence.js +272 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/README.md +373 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/advanced-custom-fields.json +101 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/back-in-stock-notifier-for-woocommerce.json +81 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/capabilities-pending-decision.json +128 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/cms-fallback-eligible.json +18 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/contact-form-7.json +104 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/facebook-for-woocommerce.json +44 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/fingerprint-aliases.json +49 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/free-gifts-for-woocommerce.json +94 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/index.json +863 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/mailpoet.json +52 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/membership-for-woocommerce.json +161 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/mwb-bookings-for-woocommerce.json +180 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/no-migration-needed.json +286 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/pricing-deals-for-woocommerce.json +79 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/product-extras-for-woocommerce.json +120 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/pw-woocommerce-gift-cards.json +128 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/requires-development.json +88 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/seo-by-rank-math.json +80 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/seriously-simple-podcasting.json +46 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/strong-testimonials.json +55 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/subscriptions-for-woocommerce.json +99 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/tablepress.json +55 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/the-events-calendar.json +160 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/ti-woocommerce-wishlist.json +81 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-checkout-field-editor-pro.json +60 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-discount-rules.json +74 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-product-bundle.json +75 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-gift-cards.json +60 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-other-payment-gateway.json +44 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-pdf-invoices-packing-slips.json +57 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-product-addon.json +141 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-progressive-discounts.json +54 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wordpress-seo.json +158 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wpc-composite-products.json +97 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wpforms-lite.json +86 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/capture-store-settings.js +90 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/plugin-knowledge-validate.js +51 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/plugin-knowledge.js +103 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-discovery.js +1668 -33
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-plugin-inventory.js +273 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/secret-value-patterns.v1.json +11 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/sub-parts.json +13 -0
- package/skills/wix-replatform/resources/rp-target-wix/SKILL.md +446 -32
- package/skills/wix-replatform/resources/rp-target-wix/app-page-routing-capabilities.json +36 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/README.md +38 -2
- package/skills/wix-replatform/resources/rp-target-wix/domains/SERVICE-PLUGINS.md +322 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/docs-survey.json +70 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/domain.json +19 -9
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/category.json +42 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/comment.json +54 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/post.json +66 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/tag.json +38 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/docs-survey.json +332 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/domain.json +75 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/booking.json +181 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/service-add-on.json +130 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/service.json +180 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/docs-survey.json +161 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/domain.json +15 -4
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/collection.json +39 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/data-item.json +47 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/reference.json +7 -1
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/docs-survey.json +204 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/domain.json +17 -4
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/contact.json +159 -15
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/email-subscription.json +80 -12
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/label.json +48 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/docs-survey.json +306 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/domain.json +30 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/delivery-profile.json +171 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/discount-rule.json +182 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-applied-discount.json +129 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-invoice-document.json +192 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item-options.json +123 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item.json +60 -12
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-merchant-note.json +112 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-payment.json +191 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-refund.json +140 -13
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order.json +383 -18
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/pickup-location.json +122 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/shipping-option.json +98 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/wishlist-item.json +86 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/docs-survey.json +128 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/domain.json +38 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/category.json +73 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/event.json +107 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/registration-form.json +89 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/rsvp.json +118 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/ticket-definition.json +93 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/docs-survey.json +86 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/domain.json +56 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/entities/form-submission.json +121 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/entities/form.json +134 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/docs-survey.json +41 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/domain.json +50 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/entities/gift-card-product.json +87 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/entities/gift-card.json +130 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/index.json +914 -24
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/docs-survey.json +41 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/domain.json +14 -4
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-file.json +45 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-folder.json +41 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/docs-survey.json +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/domain.json +13 -3
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/badge.json +41 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/custom-field.json +82 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member-about.json +77 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member.json +165 -13
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/docs-survey.json +41 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/domain.json +46 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/entities/plan-order.json +137 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/entities/plan.json +133 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/docs-survey.json +7 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/domain.json +36 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/amazon-shop-connect.json +71 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/ebay-shop-connect.json +67 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/google-youtube-connect.json +72 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/meta-catalog-connect.json +74 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/pinterest-catalog-connect.json +69 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/tiktok-catalog-connect.json +61 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/schema.json +28 -1
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/docs-survey.json +109 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/domain.json +73 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/item-seo-tags.json +148 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/seo-pattern.json +91 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/site-seo-tags.json +79 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/site/docs-survey.json +32 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/site/domain.json +49 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/site/entities/business-contact.json +103 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/docs-survey.json +203 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/domain.json +27 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/back-in-stock-request.json +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/brand.json +111 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/category.json +52 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/coupon.json +214 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/inventory-item.json +93 -13
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-customization.json +111 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-tag.json +52 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product.json +145 -14
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/promotion.json +84 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/review.json +178 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/subscription-product.json +87 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/docs-survey.json +106 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/domain.json +64 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/manual-tax-mapping.json +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-group.json +94 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-region.json +113 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-settings.json +65 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-author-attribution.js +165 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-client.js +60 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-duplicates.js +201 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-ledger.js +306 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-report.js +229 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-stage.js +55 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build.js +715 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-media.js +567 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/bulk-results.js +73 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contact-extended-fields.js +194 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contact-import.js +133 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contract-ledger.js +13 -4
- package/skills/wix-replatform/resources/rp-target-wix/lib/coupon-redemption-drift.js +491 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/customer-member.js +104 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/data-extension-schema.js +673 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/domain-knowledge.js +453 -2
- package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-balance-reconciliation.js +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-build.js +566 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-redemption-build.js +249 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-applied-discount-build.js +63 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-buyer.js +187 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-channel-build.js +64 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-history.js +1027 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-invoice-contract.js +90 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-payment-mapping.js +420 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/pricing-plan-definition.js +177 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/product-import.js +151 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/product-seo.js +723 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/seo-redirects.js +353 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/shipping-build.js +1437 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/shipping-option-stage.js +354 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/store-config-build.js +134 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-category-attach.js +221 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-inventory.js +248 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-verification.js +86 -3
- package/skills/wix-replatform/resources/rp-target-wix/lib/tax-build.js +231 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/url-mapper-transport.js +28 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/value-utils.js +14 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/variant-correlation.js +34 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-build.js +166 -15
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-target-spec.js +19 -2
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +4079 -257
- package/skills/wix-replatform/resources/rp-target-wix/lib/write-verification.js +3 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/docs-survey-sync.js +150 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge-validate.js +40 -2
- package/skills/wix-replatform/resources/rp-target-wix/scripts/verify-stores.js +6 -1
- package/skills/wix-replatform/resources/rp-telemetry/SKILL.md +148 -8
- package/skills/wix-replatform/resources/rp-telemetry/lib/bi-sink.js +83 -15
- package/skills/wix-replatform/resources/rp-telemetry/lib/telemetry-recorder.js +1119 -215
- package/skills/wix-replatform/resources/rp-telemetry/lib/transcript-digest.js +318 -0
- package/skills/wix-replatform/resources/rp-telemetry/scripts/meter-from-audit.js +2 -2
- package/skills/wix-replatform/resources/rp-telemetry/scripts/rp-telemetry.js +106 -4
- package/skills/wix-replatform/resources/rp-website-continuation/SKILL.md +94 -0
- package/skills/wix-replatform/scripts/census-reconciliation.js +25 -0
- package/skills/wix-replatform/scripts/cms-fallback.js +89 -0
- package/skills/wix-replatform/scripts/discovered-census.js +26 -0
- package/skills/wix-replatform/scripts/discovered-set-status.js +33 -0
- package/skills/wix-replatform/scripts/entity-qa-attestation.js +130 -0
- package/skills/wix-replatform/scripts/execution-health-check.js +415 -0
- package/skills/wix-replatform/scripts/execution-manifest-validate.js +1 -1
- package/skills/wix-replatform/scripts/execution-state-prepare.js +1 -1
- package/skills/wix-replatform/scripts/expectations-ledger.js +44 -0
- package/skills/wix-replatform/scripts/import-contract-verify.js +183 -0
- package/skills/wix-replatform/scripts/internal-link-audit.js +90 -0
- package/skills/wix-replatform/scripts/item-seo-tags-verify.js +205 -0
- package/skills/wix-replatform/scripts/migration-completion.js +76 -0
- package/skills/wix-replatform/scripts/orchestration-preflight.js +21 -2
- package/skills/wix-replatform/scripts/orchestration-route.js +32 -1
- package/skills/wix-replatform/scripts/orchestration-state.js +146 -10
- package/skills/wix-replatform/scripts/read-back-gate.js +175 -0
- package/skills/wix-replatform/scripts/replatform-console.js +146 -0
- package/skills/wix-replatform/scripts/replatform.js +436 -0
- package/skills/wix-replatform/scripts/source-secrets.js +77 -0
- package/skills/wix-replatform/scripts/url-redirects.js +286 -0
- package/skills/wix-replatform/scripts/user-action.js +58 -0
- package/skills/wix-replatform/scripts/website-handoff-generate.js +64 -0
- package/skills/wix-replatform/scripts/wix-dashboard-url.js +54 -0
- package/skills/wix-replatform/scripts/wix-destination.js +143 -0
- package/skills/wix-replatform/scripts/wix-headless-scaffold.js +762 -0
- package/skills/wix-vibe-headless/SKILL.md +7 -3
- package/skills/wix-vibe-headless/install/deploy.cjs +61 -34
- package/skills/wix-vibe-headless/platforms/base44.md +1 -0
- package/skills/wix-vibe-headless/references/donations/INSTRUCTIONS.md +88 -0
- package/skills/wix-vibe-headless/references/donations/app/rest/wix-donations.js +112 -0
- package/skills/wix-vibe-headless/references/donations/seed/SEED.md +11 -0
- package/skills/wix-vibe-headless/references/donations/tests/donations.test.mjs +258 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +5 -2
- package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +4 -1
- package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js +67 -3
- package/skills/wix-vibe-headless/references/shared/app/rest/wix-client.js +4 -3
- package/skills/wix-replatform/resources/rp-target-wix/SPEC-product-reviews-and-blog-authorship.md +0 -330
|
@@ -0,0 +1,1437 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isBlank } = require('./value-utils.js');
|
|
4
|
+
|
|
5
|
+
// rp-target-wix — the DETERMINISTIC WooCommerce shipping-zone -> Wix Delivery Profile payload
|
|
6
|
+
// builder. Companion to tax-build.js/discount-rule-build.js; implements domains/ecom/entities/
|
|
7
|
+
// delivery-profile.json's mappingGuidance as code.
|
|
8
|
+
//
|
|
9
|
+
// Model mismatch (why this needs its own file, not a small addition to tax-build.js):
|
|
10
|
+
// WooCommerce zones are (locations[] match rule) + (methods[], each independently priced).
|
|
11
|
+
// Wix delivery regions are (destinations[] match rule) + (deliveryCarriers[], each an APP with
|
|
12
|
+
// one backupRate). A WooCommerce zone's *methods* become Wix *delivery carriers* on the region
|
|
13
|
+
// built from that zone's *locations* — two different WooCommerce arrays fan into two different
|
|
14
|
+
// Wix arrays on the same region, not a 1:1 field rename.
|
|
15
|
+
//
|
|
16
|
+
// Input is the raw WooCommerce shape from GET /wc/v3/shipping/zones (zone), .../locations
|
|
17
|
+
// (zone.locations[], each {code, type}), and .../methods (zone.methods[], each {method_id,
|
|
18
|
+
// enabled, settings: {cost, ...}}) — already-fetched, per this codebase's "transform takes
|
|
19
|
+
// shaped input, fetching is the caller's job" convention (see tax-build.js).
|
|
20
|
+
//
|
|
21
|
+
// ONE ZONE METHOD IS NOT ONE CHECKOUT CHOICE. A WooCommerce zone lists every shipping
|
|
22
|
+
// *behaviour* as its own method row, because WooCommerce has nowhere else to put them: a
|
|
23
|
+
// minimum-order free-shipping threshold and a coupon-gated free shipping are both rows, and
|
|
24
|
+
// neither is something a buyer picks. Writing one Wix ShippingOption per zone method therefore
|
|
25
|
+
// reproduces — and multiplies — a display defect the source already has. This builder resolves
|
|
26
|
+
// zone methods to the merchant's actual policy first (see planShippingZones), and reports every
|
|
27
|
+
// resolution it made so a human can confirm the intent was read correctly.
|
|
28
|
+
|
|
29
|
+
// "Basic Shipping" is Wix's own first-party, non-carrier-integration delivery app — the direct
|
|
30
|
+
// equivalent of WooCommerce's flat_rate/free_shipping (a merchant-set price, no real courier
|
|
31
|
+
// calculation). Per dev.wix.com's own Add Delivery Carrier example (not a placeholder GUID — the
|
|
32
|
+
// docs' worked example uses this exact id) and confirmed installed on a live site under
|
|
33
|
+
// displayName "Basic Shipping". Same "fixed platform constant" reasoning as discount-rule-build.js's
|
|
34
|
+
// WIX_STORES_APP_ID: apps Wix itself created keep one appId across every site. Still overridable.
|
|
35
|
+
const BASIC_SHIPPING_APP_ID = '45c44b27-ca7b-4891-8c0d-1747d588b835';
|
|
36
|
+
|
|
37
|
+
// WooCommerce's own continent -> member-country-code table (plugins/woocommerce/i18n/
|
|
38
|
+
// continents.php, github.com/woocommerce/woocommerce, fetched 2026-08-12), needed because a WC
|
|
39
|
+
// zone location can be `type: "continent"` (e.g. `EU`) but Wix's Destination object has no
|
|
40
|
+
// continent concept — only countryCode (+ optional subdivisions). A continent-type location is
|
|
41
|
+
// expanded to one Destination per member country. Only continents with a real zone hit in
|
|
42
|
+
// practice need to be complete here; unlisted continent codes fall through to a flagged gap
|
|
43
|
+
// rather than silently matching nothing.
|
|
44
|
+
const CONTINENT_COUNTRIES = {
|
|
45
|
+
EU: ['AD', 'AL', 'AT', 'AX', 'BA', 'BE', 'BG', 'BY', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FO', 'FR', 'GB', 'GG', 'GI', 'GR', 'HR', 'HU', 'IE', 'IM', 'IS', 'IT', 'JE', 'LI', 'LT', 'LU', 'LV', 'MC', 'MD', 'ME', 'MK', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'RU', 'SE', 'SI', 'SJ', 'SK', 'SM', 'TR', 'UA', 'VA', 'XK'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function isNumericString(value) {
|
|
49
|
+
return typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toAmountString(value) {
|
|
53
|
+
return String(Number(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- WC zone locations -> Wix destinations --------------------------------------------------
|
|
57
|
+
// WC location.code shapes, per WC_Shipping_Zone::get_zone_locations(): type "country" -> "US";
|
|
58
|
+
// type "state" -> "US:CA" (country-colon-state); type "continent" -> "EU"; type "postcode" ->
|
|
59
|
+
// a literal postcode/pattern, which Wix's Destination has no equivalent for at all (country/
|
|
60
|
+
// subdivision only) and is always a gap.
|
|
61
|
+
function normalizeZoneLocations(locations) {
|
|
62
|
+
const destinations = [];
|
|
63
|
+
const gaps = [];
|
|
64
|
+
for (const location of locations || []) {
|
|
65
|
+
const type = String(location?.type || '').trim();
|
|
66
|
+
const code = String(location?.code || '').trim();
|
|
67
|
+
if (!code) continue;
|
|
68
|
+
if (type === 'country') {
|
|
69
|
+
destinations.push({ countryCode: code.toUpperCase() });
|
|
70
|
+
} else if (type === 'state') {
|
|
71
|
+
const [country, state] = code.split(':');
|
|
72
|
+
if (!country || !state) {
|
|
73
|
+
gaps.push({ location, reason: `state-type location code "${code}" did not parse as COUNTRY:STATE` });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
destinations.push({ countryCode: country.toUpperCase(), subdivisions: [`${country.toUpperCase()}-${state.toUpperCase()}`] });
|
|
77
|
+
} else if (type === 'continent') {
|
|
78
|
+
const countries = CONTINENT_COUNTRIES[code.toUpperCase()];
|
|
79
|
+
if (!countries) {
|
|
80
|
+
gaps.push({ location, reason: `continent code "${code}" has no country table here (add it to CONTINENT_COUNTRIES if this recurs)` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const countryCode of countries) destinations.push({ countryCode });
|
|
84
|
+
} else if (type === 'postcode') {
|
|
85
|
+
gaps.push({ location, reason: 'postcode/postal-pattern zone matching has no Wix Destination equivalent (country/subdivision only) — reconfigure-in-wix' });
|
|
86
|
+
} else {
|
|
87
|
+
gaps.push({ location, reason: `unrecognized WooCommerce zone-location type "${type}"` });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { destinations, gaps };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Conditional rates -------------------------------------------------------------------------
|
|
94
|
+
// ShippingOption.rates and PickupLocation.rates are the SAME type: ConditionalRates, from
|
|
95
|
+
// com.wix.ecom.delivery.rates.v1. Shape (authoritative, read off the service definition, not
|
|
96
|
+
// the public docs):
|
|
97
|
+
// ConditionalRates { conditions[0..10], amount (decimal string >= 0, max 3 dp), multiplyByQuantity }
|
|
98
|
+
// Condition { type: BY_TOTAL_WEIGHT|BY_TOTAL_PRICE|BY_TOTAL_QUANTITY, value, operator: EQ|GT|GTE|LT|LTE }
|
|
99
|
+
// Conditions AND together; an empty conditions[] means "always applies". The runtime rule,
|
|
100
|
+
// verbatim from the definition: "at runtime for a given shipment input, up to one rate (price)
|
|
101
|
+
// should be returned in an option. If more than one rate is valid then we return the lowest one."
|
|
102
|
+
//
|
|
103
|
+
// This is what makes a source minimum-order free-shipping threshold migratable rather than a gap.
|
|
104
|
+
//
|
|
105
|
+
// LIVE-VERIFIED 2026-09-16: the explicit two-way LT/GTE partition below
|
|
106
|
+
// (priceThresholdRates) was originally written on the theory that it would make the merchant's
|
|
107
|
+
// dashboard show the same two bands the source had. That theory was never live-checked and is
|
|
108
|
+
// WRONG: the Wix dashboard's own rate editor renders it as "Rate by price" mode with invalid,
|
|
109
|
+
// unfillable "From"/"Up to" range fields (confirmed via screenshot — a merchant opening this
|
|
110
|
+
// rate sees validation errors), even though the write succeeds and the checkout economics are
|
|
111
|
+
// correct. The combination that renders correctly, as the dashboard's simpler "Flat rate" mode
|
|
112
|
+
// with its "Offer free shipping when customers purchase a certain amount" checkbox (confirmed
|
|
113
|
+
// live), is `[unconditionalRate(paidAmount), ...freeAboveThresholdRates(thresholdAmount)]` —
|
|
114
|
+
// i.e. LEANING ON the lowest-wins tiebreak this comment used to say to avoid.
|
|
115
|
+
// resolveZoneMethods uses that combination, not priceThresholdRates, for exactly this reason.
|
|
116
|
+
// priceThresholdRates is kept below (still schema-valid, still economically correct — the
|
|
117
|
+
// dashboard-editability problem is cosmetic, not a data-correctness one) for any caller that
|
|
118
|
+
// only needs the rates[] shape and does not need it to open cleanly in the dashboard's own
|
|
119
|
+
// editor; do not reach for it as the default without re-verifying that live first.
|
|
120
|
+
const CONDITION_BY_TOTAL_PRICE = 'BY_TOTAL_PRICE';
|
|
121
|
+
|
|
122
|
+
function unconditionalRate(amount) {
|
|
123
|
+
return { conditions: [], amount: String(amount), multiplyByQuantity: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function priceThresholdRates(paidAmount, thresholdAmount) {
|
|
127
|
+
return [
|
|
128
|
+
{ conditions: [{ type: CONDITION_BY_TOTAL_PRICE, operator: 'LT', value: String(thresholdAmount) }], amount: String(paidAmount), multiplyByQuantity: false },
|
|
129
|
+
{ conditions: [{ type: CONDITION_BY_TOTAL_PRICE, operator: 'GTE', value: String(thresholdAmount) }], amount: '0', multiplyByQuantity: false },
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function freeAboveThresholdRates(thresholdAmount) {
|
|
134
|
+
return [
|
|
135
|
+
{ conditions: [{ type: CONDITION_BY_TOTAL_PRICE, operator: 'GTE', value: String(thresholdAmount) }], amount: '0', multiplyByQuantity: false },
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The one thing the service definition does NOT settle: whether BY_TOTAL_PRICE is measured
|
|
140
|
+
// before or after cart discounts. WooCommerce's own free_shipping has an `ignore_discounts`
|
|
141
|
+
// setting for exactly this, so the two platforms can disagree. Ship the threshold as written,
|
|
142
|
+
// carry this note on every option built from one, and confirm it at the first live checkout —
|
|
143
|
+
// it is a decision, not a measurement, and must never be reported as verified.
|
|
144
|
+
function thresholdDiscountBasisNote(ignoreDiscounts) {
|
|
145
|
+
const sourceBasis = String(ignoreDiscounts) === 'yes' ? 'the pre-discount subtotal' : 'the post-discount subtotal';
|
|
146
|
+
return `Threshold basis unverified: WooCommerce compares ${sourceBasis} (ignore_discounts="${ignoreDiscounts ?? 'no'}"); Wix's BY_TOTAL_PRICE does not document which side it uses. Shipped as written — confirm at the first live checkout, and do not report this rate as verified until then.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- WC shipping method -> a classification ----------------------------------------------------
|
|
150
|
+
// Kinds, and why each exists:
|
|
151
|
+
// 'carrier' — a real buyer-facing delivery choice with a price. carrierRole 'basic'.
|
|
152
|
+
// 'threshold' — NOT a choice. A minimum-order free-shipping rule that belongs ON the paid
|
|
153
|
+
// options in the same zone (see planShippingZones), never as its own row.
|
|
154
|
+
// 'coupon-domain' — NOT a choice, and NOT a shipping setting at all. Coupon-gated free shipping
|
|
155
|
+
// lives entirely on the Wix coupon; the WooCommerce zone row exists only
|
|
156
|
+
// because WooCommerce needs one to switch the behaviour on.
|
|
157
|
+
// 'pickup' — NOT a shipping option. A Wix PickupLocation (see buildPickupLocationInput);
|
|
158
|
+
// writing it as a ShippingOption puts a 0-priced *delivery* next to the real
|
|
159
|
+
// paid one, which is how collection ends up looking like free shipping.
|
|
160
|
+
// 'gap' — no data equivalent; reported, never guessed at.
|
|
161
|
+
//
|
|
162
|
+
// Confidence, verified against WooCommerce core's own shipping method implementations
|
|
163
|
+
// (plugins/woocommerce/includes/shipping/class-wc-shipping-{flat-rate,free-shipping,local-pickup}.php):
|
|
164
|
+
// - HIGH: flat_rate's base `cost` setting -> a paid option. Real, always-applied.
|
|
165
|
+
// - HIGH: free_shipping's `requires` decides everything. Empty means genuinely always free —
|
|
166
|
+
// the ONE case where an unconditional 0 is correct.
|
|
167
|
+
// - HIGH: local_pickup / pickup_location -> a Wix PickupLocation.
|
|
168
|
+
// - GAP, no calculation engine to replicate: any other method_id (e.g. a real-carrier plugin
|
|
169
|
+
// like a UPS/FedEx integration) — carrier-calculated rates have no data equivalent.
|
|
170
|
+
// - Per-shipping-class cost overrides (flat_rate's `class_cost_*` settings) are a known,
|
|
171
|
+
// flagged gap: Wix's additionalCharges apply to every order in the region, not conditionally
|
|
172
|
+
// per product shipping class, so mapping a class-specific cost there would overcharge every
|
|
173
|
+
// other class. Recorded in `notes[]`, never auto-applied.
|
|
174
|
+
function classifyMethod(method) {
|
|
175
|
+
const methodId = String(method?.method_id || '').trim();
|
|
176
|
+
const enabled = method?.enabled !== false;
|
|
177
|
+
const title = String(method?.settings?.title?.value || method?.method_title || methodId).trim();
|
|
178
|
+
const notes = [];
|
|
179
|
+
|
|
180
|
+
const classCostKeys = Object.keys(method?.settings || {}).filter((key) => /^class_cost_\d+$/.test(key) && !isBlank(method.settings[key]?.value));
|
|
181
|
+
if (classCostKeys.length > 0) {
|
|
182
|
+
notes.push(`Per-shipping-class cost overrides present (${classCostKeys.join(', ')}) — not applied; Wix additionalCharges are unconditional per-order, not per-product-class, so mapping these would overcharge every other class. Verify manually.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (methodId === 'flat_rate') {
|
|
186
|
+
const cost = method?.settings?.cost?.value;
|
|
187
|
+
if (!isNumericString(cost)) {
|
|
188
|
+
return { kind: 'gap', enabled, title, reason: `flat_rate cost "${cost}" is not a plain numeric value (formula costs like "10 * [qty]" have no static Wix equivalent)`, notes };
|
|
189
|
+
}
|
|
190
|
+
return { kind: 'carrier', carrierRole: 'basic', enabled, title, amount: toAmountString(cost), notes };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (methodId === 'free_shipping') {
|
|
194
|
+
const requires = String(method?.settings?.requires?.value || '').trim();
|
|
195
|
+
const minAmount = method?.settings?.min_amount?.value;
|
|
196
|
+
const ignoreDiscounts = method?.settings?.ignore_discounts?.value;
|
|
197
|
+
|
|
198
|
+
if (requires === '') {
|
|
199
|
+
// Genuinely unconditional. The only shape where a 0-amount, condition-free option is right.
|
|
200
|
+
return { kind: 'carrier', carrierRole: 'basic', enabled, title, amount: '0', notes };
|
|
201
|
+
}
|
|
202
|
+
if (requires === 'coupon') {
|
|
203
|
+
notes.push('Coupon-gated free shipping has no Wix delivery-settings counterpart: at checkout the buyer sees the normal delivery price and the coupon removes it. Handled entirely in the coupon domain — see resolveFreeShippingCouponCoverage. Nothing is lost, so this is NOT an unmigrated gap.');
|
|
204
|
+
return { kind: 'coupon-domain', enabled, title, notes };
|
|
205
|
+
}
|
|
206
|
+
if (requires === 'both') {
|
|
207
|
+
// "minimum order AND a coupon". Wix rate conditions can express the amount but have no
|
|
208
|
+
// coupon term, so a threshold-only migration would hand out free shipping the source
|
|
209
|
+
// withheld. Being MORE generous than the source is the same class of defect as being
|
|
210
|
+
// unconditional — report it, never guess it.
|
|
211
|
+
return {
|
|
212
|
+
kind: 'gap',
|
|
213
|
+
enabled,
|
|
214
|
+
title,
|
|
215
|
+
reason: `free_shipping requires="both" needs a minimum order (${minAmount}) AND a coupon; a Wix rate condition can carry the amount but has no coupon term, so migrating the threshold alone would give free shipping the source withholds. Needs a merchant decision — reconfigure-in-wix.`,
|
|
216
|
+
notes,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (requires === 'min_amount' || requires === 'either') {
|
|
220
|
+
if (!isNumericString(minAmount)) {
|
|
221
|
+
return { kind: 'gap', enabled, title, reason: `free_shipping requires="${requires}" but min_amount "${minAmount}" is not numeric — there is no threshold to build`, notes };
|
|
222
|
+
}
|
|
223
|
+
if (requires === 'either') {
|
|
224
|
+
notes.push(`free_shipping requires="either" means the minimum order OR a coupon. The ${minAmount} threshold migrates here; the coupon half is a coupon-domain concern (a Wix freeShipping coupon covers it).`);
|
|
225
|
+
}
|
|
226
|
+
notes.push(thresholdDiscountBasisNote(ignoreDiscounts));
|
|
227
|
+
return { kind: 'threshold', enabled, title, thresholdAmount: toAmountString(minAmount), amount: '0', notes };
|
|
228
|
+
}
|
|
229
|
+
return { kind: 'gap', enabled, title, reason: `free_shipping requires="${requires}" is not a recognized WooCommerce value`, notes };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (methodId === 'local_pickup' || methodId === 'pickup_location') {
|
|
233
|
+
const cost = method?.settings?.cost?.value;
|
|
234
|
+
const amount = isNumericString(cost) ? toAmountString(cost) : '0';
|
|
235
|
+
if (!isBlank(cost) && !isNumericString(cost)) {
|
|
236
|
+
notes.push(`local_pickup cost "${cost}" is not a plain numeric value — the pickup location is created at 0 instead. Verify manually.`);
|
|
237
|
+
}
|
|
238
|
+
return { kind: 'pickup', enabled, title, amount, notes };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { kind: 'gap', enabled, title, reason: `method_id "${methodId}" has no calculation engine to replicate — likely a real-carrier integration; carrier-calculated rates have no Wix data equivalent (reconfigure-in-wix)`, notes };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- Delivery carrier ---------------------------------------------------------------------------
|
|
245
|
+
// `appId` resolved live by the caller (see resolveBasicShippingAppId/resolvePickupAppId in
|
|
246
|
+
// wix-writers.js) — required, never silently defaulted to undefined.
|
|
247
|
+
//
|
|
248
|
+
// A carrier attaches an APP to a region and supplies a fallback price; it is not the rate the
|
|
249
|
+
// buyer sees (that is the ShippingOption).
|
|
250
|
+
//
|
|
251
|
+
// PICKUP IS THE EXCEPTION, and it cuts both ways — LIVE-LEARNED 2026-09-01, both halves the
|
|
252
|
+
// hard way. An ACTIVE Pickup backupRate renders as a 0-priced DELIVERY row beside the paid
|
|
253
|
+
// one. But removing the Pickup carrier ENTIRELY made the collection point vanish from
|
|
254
|
+
// checkout, even with a live, active PickupLocation bound to the region. The region needs the
|
|
255
|
+
// app attached in order to render collection at all. So a pickup gets a carrier via
|
|
256
|
+
// buildPickupCarrierInput below — amount '0', active FALSE — never through this function, and
|
|
257
|
+
// never with an active rate.
|
|
258
|
+
function buildDeliveryCarrierInput(classified, { appId }) {
|
|
259
|
+
if (!appId) throw new Error('buildDeliveryCarrierInput: appId is required (resolve live for non-Basic-Shipping carriers)');
|
|
260
|
+
if (classified.kind !== 'carrier') throw new Error(`buildDeliveryCarrierInput: called on a non-carrier classification (${classified.kind}) — only a real paid/free delivery choice gets an active carrier; pickup uses buildPickupCarrierInput (inactive rate) and a threshold folds into a sibling option`);
|
|
261
|
+
return {
|
|
262
|
+
appId,
|
|
263
|
+
backupRate: {
|
|
264
|
+
title: classified.title || 'Shipping',
|
|
265
|
+
amount: classified.amount,
|
|
266
|
+
active: true,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// The Pickup carrier attachment: present so the region can RENDER collection, with an
|
|
272
|
+
// INACTIVE backup rate so the carrier itself never becomes a delivery row. Both halves matter
|
|
273
|
+
// and each was learned by breaking a live store in the opposite direction. `appId` is
|
|
274
|
+
// installation-scoped — resolve it live with wix-writers.js's resolvePickupAppId, never
|
|
275
|
+
// hardcode it. The PickupLocation is still what describes the actual collection point.
|
|
276
|
+
function buildPickupCarrierInput(classified, { appId }) {
|
|
277
|
+
if (!appId) throw new Error('buildPickupCarrierInput: appId is required (resolve the Pickup app live with resolvePickupAppId)');
|
|
278
|
+
if (classified.kind !== 'pickup') throw new Error(`buildPickupCarrierInput: called on a non-pickup classification (${classified.kind})`);
|
|
279
|
+
return {
|
|
280
|
+
appId,
|
|
281
|
+
backupRate: {
|
|
282
|
+
title: classified.title || 'Pickup',
|
|
283
|
+
amount: '0',
|
|
284
|
+
// NEVER true. An active pickup backup rate is a 0-priced delivery row beside the paid one.
|
|
285
|
+
active: false,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Shipping option ----------------------------------------------------------------------------
|
|
291
|
+
// LIVE-DISCOVERED: a deliveryCarrier's backupRate (buildDeliveryCarrierInput above) does NOT make
|
|
292
|
+
// Wix consider a region as having a working rate at checkout, and does NOT clear Wix's own "This
|
|
293
|
+
// region is missing rates" dashboard warning. That is driven by a SEPARATE resource,
|
|
294
|
+
// ShippingOption (`/ecom/v1/shipping-options`). A region migration needs BOTH — never one
|
|
295
|
+
// without the other, for every 'carrier'-kind method.
|
|
296
|
+
//
|
|
297
|
+
// FIELD NAMES, read off the service definition (wix-private/ecom shipping_option.proto):
|
|
298
|
+
// - `estimatedDeliveryTime` (field 12, `estimated_delivery_time`, max 500). The proto's
|
|
299
|
+
// `translatable` annotation names it `estimate_delivery_time` -- a key in the translation
|
|
300
|
+
// system, NOT a request field. Wix accepts the misspelled `estimateDeliveryTime` with a 200 and
|
|
301
|
+
// silently drops it; that is how every migrated option lost its delivery time unnoticed. The
|
|
302
|
+
// builder never emits the misspelled key.
|
|
303
|
+
// - `deliveryRegionIds` (plural). The singular `deliveryRegionId` is deprecated with a removal
|
|
304
|
+
// target already in the past. Both are sent: the plural is what is honoured going forward,
|
|
305
|
+
// the singular keeps older reads/tooling working until it is actually removed.
|
|
306
|
+
// There is NO default estimate here. The text is resolved per option by resolveDeliveryEstimate
|
|
307
|
+
// (merchant > source > a reviewed default in the DESTINATION site's language > none), so an
|
|
308
|
+
// English promise can never reach a non-English checkout by falling through a default argument.
|
|
309
|
+
const ESTIMATED_DELIVERY_TIME_MAX = 500;
|
|
310
|
+
function buildShippingOptionInput(classified, { deliveryRegionId, estimatedDeliveryTime, estimateDeliveryTime, rates } = {}) {
|
|
311
|
+
if (!deliveryRegionId) throw new Error('buildShippingOptionInput: deliveryRegionId is required');
|
|
312
|
+
if (classified.kind !== 'carrier') throw new Error(`buildShippingOptionInput: called on a non-carrier classification (${classified.kind}) — a pickup method must become a PickupLocation, not a shipping option`);
|
|
313
|
+
// The legacy (wrong) key is accepted as an input alias for one release so an older caller keeps
|
|
314
|
+
// working -- but it is translated, never emitted, and a conflicting pair is a caller bug.
|
|
315
|
+
if (estimatedDeliveryTime !== undefined && estimateDeliveryTime !== undefined && estimatedDeliveryTime !== estimateDeliveryTime) {
|
|
316
|
+
throw new Error('buildShippingOptionInput: estimatedDeliveryTime and the legacy estimateDeliveryTime were both given with different values');
|
|
317
|
+
}
|
|
318
|
+
const estimate = estimatedDeliveryTime !== undefined ? estimatedDeliveryTime : estimateDeliveryTime;
|
|
319
|
+
if (estimate !== undefined && estimate !== null && typeof estimate !== 'string') {
|
|
320
|
+
throw new Error('buildShippingOptionInput: estimatedDeliveryTime must be a string');
|
|
321
|
+
}
|
|
322
|
+
if (typeof estimate === 'string' && estimate.length > ESTIMATED_DELIVERY_TIME_MAX) {
|
|
323
|
+
throw new Error(`buildShippingOptionInput: estimatedDeliveryTime is ${estimate.length} characters; the service maximum is ${ESTIMATED_DELIVERY_TIME_MAX} (never truncated)`);
|
|
324
|
+
}
|
|
325
|
+
const resolvedRates = Array.isArray(rates) && rates.length > 0 ? rates : [unconditionalRate(classified.amount)];
|
|
326
|
+
return {
|
|
327
|
+
title: classified.title || 'Shipping',
|
|
328
|
+
...(typeof estimate === 'string' && estimate !== '' ? { estimatedDeliveryTime: estimate } : {}),
|
|
329
|
+
deliveryRegionId,
|
|
330
|
+
deliveryRegionIds: [deliveryRegionId],
|
|
331
|
+
rates: resolvedRates,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- Pickup location ----------------------------------------------------------------------------
|
|
336
|
+
// A Wix PickupLocation is its own entity, not a shipping option and not a delivery carrier.
|
|
337
|
+
// POST https://www.wixapis.com/pickup-locations/v1/pickup-locations, permission
|
|
338
|
+
// ECOM.PICKUP_LOCATION_CREATE. `address` is REQUIRED by the create call.
|
|
339
|
+
//
|
|
340
|
+
// The address is supplied by the caller, because where it comes from depends on the source
|
|
341
|
+
// shape — see resolvePickupAddressSource above. A source using Blocks Local Pickup already has
|
|
342
|
+
// real addresses; a source using the LEGACY local_pickup method has none anywhere, and only the
|
|
343
|
+
// merchant knows. Throwing here is deliberate either way: a silently address-less pickup
|
|
344
|
+
// location fails server-side with far less context, and inventing one is worse than stopping.
|
|
345
|
+
//
|
|
346
|
+
// `shippingRuleId` is deliberately NOT sent. It is deprecated and marked internal, replaced by
|
|
347
|
+
// `deliveryRegionIds`, with a removal target already in the past — the Wix dashboard still sends
|
|
348
|
+
// it, which is not a reason for a migration to.
|
|
349
|
+
//
|
|
350
|
+
// `amount` overrides the classification's fee, and exists because the two pickup shapes keep the
|
|
351
|
+
// fee in different places. LEGACY local_pickup carries it on the zone method, so
|
|
352
|
+
// classifyZoneMethod already has it. BLOCKS pickup carries it once, globally, in
|
|
353
|
+
// `pickup_location_settings.cost` — which the zone method does not have and the classification
|
|
354
|
+
// therefore reads as 0. A Blocks caller passes the resolved location's `amount` here; a legacy
|
|
355
|
+
// caller passes nothing and keeps the classified value.
|
|
356
|
+
function buildPickupLocationInput(classified, { deliveryRegionIds, address, deliveryTime, instructions, active = true, amount } = {}) {
|
|
357
|
+
if (classified.kind !== 'pickup') throw new Error(`buildPickupLocationInput: called on a non-pickup classification (${classified.kind})`);
|
|
358
|
+
if (!Array.isArray(deliveryRegionIds) || deliveryRegionIds.length === 0) {
|
|
359
|
+
throw new Error('buildPickupLocationInput: deliveryRegionIds is required (a pickup location must be bound to at least one delivery region)');
|
|
360
|
+
}
|
|
361
|
+
if (!address || isBlank(address.country)) {
|
|
362
|
+
throw new Error('buildPickupLocationInput: address with a country is required — WooCommerce\'s local_pickup method carries no address, so the caller must supply one from the store address or the merchant');
|
|
363
|
+
}
|
|
364
|
+
const name = classified.title || 'Pickup';
|
|
365
|
+
return {
|
|
366
|
+
name,
|
|
367
|
+
address,
|
|
368
|
+
...(isBlank(deliveryTime) ? {} : { deliveryTime: String(deliveryTime) }),
|
|
369
|
+
...(isBlank(instructions) ? {} : { instructions: String(instructions) }),
|
|
370
|
+
active,
|
|
371
|
+
rates: [unconditionalRate(isBlank(amount) ? (classified.amount || '0') : String(amount))],
|
|
372
|
+
deliveryRegionIds: [...deliveryRegionIds],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- Where a collection address actually comes from ---------------------------------------------
|
|
377
|
+
// WooCommerce has TWO local-pickup implementations, and they differ on the one thing that
|
|
378
|
+
// matters here — whether an address exists in the source at all:
|
|
379
|
+
//
|
|
380
|
+
// 1. BLOCKS Local Pickup (`pickup_location` method). Real locations WITH addresses, stored as
|
|
381
|
+
// WordPress options and exposed on `GET /wp/v2/settings` as `pickup_locations[]`:
|
|
382
|
+
// { name, address: { address_1, city, state, postcode, country }, details, enabled }
|
|
383
|
+
// plus `pickup_location_settings` = { enabled, title, tax_status, cost }. Note the address
|
|
384
|
+
// shape here keeps `state` SEPARATE from `country` — unlike the store address's
|
|
385
|
+
// `woocommerce_default_country`, which packs both into one COUNTRY:STATE string.
|
|
386
|
+
//
|
|
387
|
+
// 2. LEGACY `local_pickup` zone method. Settings are only { title, tax_status, cost }. There is
|
|
388
|
+
// no address, no linked record, nothing to read. The town is often named in the method's own
|
|
389
|
+
// title text — that is a label, not an address, and must never be parsed as one.
|
|
390
|
+
//
|
|
391
|
+
// So "the source has no pickup address" is true only for the legacy shape. Check the Blocks
|
|
392
|
+
// locations FIRST; a store using them needs no human input at all.
|
|
393
|
+
// WooCommerce writes option booleans as the strings 'yes'/'no', but the same field arrives as a
|
|
394
|
+
// real boolean on some routes and is simply absent on others. Absent means "not configured", which
|
|
395
|
+
// for a global feature switch has to read as ON — a store with pickup locations and no explicit
|
|
396
|
+
// flag is using them. Only an explicit no/false turns it off.
|
|
397
|
+
function isExplicitlyDisabled(value) {
|
|
398
|
+
if (value === false) return true;
|
|
399
|
+
if (typeof value !== 'string') return false;
|
|
400
|
+
const normalized = value.trim().toLowerCase();
|
|
401
|
+
return normalized === 'no' || normalized === 'false' || normalized === '0' || normalized === 'off';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// `pickup_location_settings` is the GLOBAL half of Blocks Local Pickup and it owns two things no
|
|
405
|
+
// individual location carries:
|
|
406
|
+
//
|
|
407
|
+
// enabled — the master switch. Every location can be individually `enabled: true` while pickup
|
|
408
|
+
// as a whole is off, and reading only the per-location flag then re-enables a
|
|
409
|
+
// fulfillment method the merchant deliberately turned off.
|
|
410
|
+
// cost — the collection fee, set ONCE for all locations. Blocks pickup has no per-zone cost
|
|
411
|
+
// field, so `method.settings.cost` (which classifyZoneMethod reads, correctly, for the
|
|
412
|
+
// LEGACY local_pickup zone method) is absent for this shape. Without reading it here
|
|
413
|
+
// the fee silently becomes 0 and every pickup order loses it.
|
|
414
|
+
//
|
|
415
|
+
// Both fields sit in the payload the default discovery sweep already captures, so nothing extra
|
|
416
|
+
// has to be fetched — they simply have to be read.
|
|
417
|
+
function pickupLocationsFromWpSettings(wpSettings) {
|
|
418
|
+
const raw = wpSettings?.pickup_locations;
|
|
419
|
+
if (!Array.isArray(raw)) return [];
|
|
420
|
+
const globalSettings = wpSettings?.pickup_location_settings;
|
|
421
|
+
// The option is an object when configured; WooCommerce serializes an unconfigured one as [].
|
|
422
|
+
const global = globalSettings && typeof globalSettings === 'object' && !Array.isArray(globalSettings) ? globalSettings : {};
|
|
423
|
+
const globallyDisabled = isExplicitlyDisabled(global.enabled);
|
|
424
|
+
const globalCost = global.cost;
|
|
425
|
+
const amount = isNumericString(String(globalCost ?? '')) ? toAmountString(globalCost) : '0';
|
|
426
|
+
return raw
|
|
427
|
+
.filter((entry) => entry && typeof entry === 'object')
|
|
428
|
+
.map((entry) => {
|
|
429
|
+
const source = entry.address || {};
|
|
430
|
+
const address = {
|
|
431
|
+
...(isBlank(source.country) ? {} : { country: String(source.country).trim().toUpperCase() }),
|
|
432
|
+
...(isBlank(source.state) ? {} : { subdivision: String(source.state).trim() }),
|
|
433
|
+
...(isBlank(source.city) ? {} : { city: String(source.city).trim() }),
|
|
434
|
+
...(isBlank(source.postcode) ? {} : { postalCode: String(source.postcode).trim() }),
|
|
435
|
+
// address_1 is one free-text line and Wix's addressLine is the matching free-text field.
|
|
436
|
+
// Same discipline as the store address: never parse a house number out of it.
|
|
437
|
+
...(isBlank(source.address_1) ? {} : { addressLine: String(source.address_1).trim() }),
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
name: isBlank(entry.name) ? '' : String(entry.name).trim(),
|
|
441
|
+
address: Object.keys(address).length > 0 ? address : null,
|
|
442
|
+
instructions: isBlank(entry.details) ? '' : String(entry.details).trim(),
|
|
443
|
+
// Both gates must pass. The master switch cannot be overridden by a per-location flag.
|
|
444
|
+
active: !globallyDisabled && entry.enabled !== false,
|
|
445
|
+
// The global fee, carried per location so the caller passes it straight to
|
|
446
|
+
// buildPickupLocationInput's `amount` without having to know where it came from.
|
|
447
|
+
amount,
|
|
448
|
+
...(globallyDisabled ? { globallyDisabled: true } : {}),
|
|
449
|
+
};
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// A pickup location with NO street address is a real merchant choice, not a failure. Plenty of
|
|
454
|
+
// shops coordinate collection by phone and deliberately do not publish a street — the source
|
|
455
|
+
// method's own title usually says so ("contact us to arrange collection"). Wix's create call
|
|
456
|
+
// REQUIRES the `address` object, so "blank" cannot mean absent; the honest minimum is the one
|
|
457
|
+
// part we genuinely know — the country, taken from the delivery region this pickup is bound to.
|
|
458
|
+
//
|
|
459
|
+
// Derived, never guessed: if the region's destinations span more than one country there is no
|
|
460
|
+
// single answer and this throws rather than picking one.
|
|
461
|
+
function buildMinimalPickupAddress(destinations) {
|
|
462
|
+
const countries = [...new Set((destinations || []).map((d) => String(d?.countryCode || '').trim().toUpperCase()).filter(Boolean))];
|
|
463
|
+
if (countries.length === 0) {
|
|
464
|
+
throw new Error('buildMinimalPickupAddress: the delivery region has no destination country to derive a minimal address from');
|
|
465
|
+
}
|
|
466
|
+
if (countries.length > 1) {
|
|
467
|
+
throw new Error(`buildMinimalPickupAddress: the delivery region spans ${countries.length} countries (${countries.join(', ')}) — there is no single country for a collection point; ask the merchant`);
|
|
468
|
+
}
|
|
469
|
+
return { country: countries[0] };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Resolves the address question for a zone's pickup methods, in the order that avoids asking a
|
|
473
|
+
// human anything the source already answered:
|
|
474
|
+
// 'blocks' — the source carries real pickup locations with addresses. Build them directly.
|
|
475
|
+
// 'minimal' — the merchant was asked and declined to publish a street address. Country only,
|
|
476
|
+
// derived from the region; the location's NAME carries where and how to collect.
|
|
477
|
+
// An explicit choice, only ever reached via `addressDeclined`.
|
|
478
|
+
// 'proposal' — legacy method only. Nothing in the source has an address, so the store's own
|
|
479
|
+
// address (see store-config-build.js) is offered for a human to CONFIRM or
|
|
480
|
+
// replace. It is a starting point, never an answer: a shop's registered address
|
|
481
|
+
// and its collection point are routinely different places.
|
|
482
|
+
// 'ask' — legacy method and no store address captured either. Ask outright.
|
|
483
|
+
// The caller never invents an address in any branch.
|
|
484
|
+
function resolvePickupAddressSource({ pickupPlans = [], wpSettings = null, storeAddress = null, addressDeclined = false, destinations = [] } = {}) {
|
|
485
|
+
const blocks = pickupLocationsFromWpSettings(wpSettings).filter((location) => location.address);
|
|
486
|
+
if (blocks.length > 0) {
|
|
487
|
+
return { strategy: 'blocks', locations: blocks, needsConfirmation: false };
|
|
488
|
+
}
|
|
489
|
+
if ((pickupPlans || []).length === 0) {
|
|
490
|
+
return { strategy: 'none', locations: [], needsConfirmation: false };
|
|
491
|
+
}
|
|
492
|
+
if (addressDeclined) {
|
|
493
|
+
return {
|
|
494
|
+
strategy: 'minimal',
|
|
495
|
+
locations: [],
|
|
496
|
+
minimalAddress: buildMinimalPickupAddress(destinations),
|
|
497
|
+
needsConfirmation: false,
|
|
498
|
+
note: 'Address deliberately left incomplete at the merchant\'s request: country only, derived from the delivery region. The location name is what tells buyers where and how to collect. Wix accepts this because only the address OBJECT is required, not its parts — but a country-only collection point has not been verified for how it renders at checkout, so look at it once live.',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (storeAddress) {
|
|
502
|
+
return {
|
|
503
|
+
strategy: 'proposal',
|
|
504
|
+
locations: [],
|
|
505
|
+
proposedAddress: storeAddress,
|
|
506
|
+
needsConfirmation: true,
|
|
507
|
+
question: 'The source has a legacy pickup method, which carries no address. The store\'s own registered address is proposed below — confirm it is where customers collect, or replace it. These are routinely different places.',
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
strategy: 'ask',
|
|
512
|
+
locations: [],
|
|
513
|
+
needsConfirmation: true,
|
|
514
|
+
question: 'The source has a legacy pickup method, which carries no address, and no store address was captured either. Ask the merchant where customers collect.',
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// --- Coupon-gated free shipping: look first, create last -----------------------------------------
|
|
519
|
+
// Decision: coupon-gated free shipping is carried by a coupon, so *something* has to exist — but
|
|
520
|
+
// this pipeline never invents one. Resolution order:
|
|
521
|
+
// 1. source — the source already has coupons carrying the free-shipping flag; the coupon
|
|
522
|
+
// pipeline is already moving them. Create nothing, record which codes cover it.
|
|
523
|
+
// 2. target — already present on the destination (a re-run, a partial earlier run, or a
|
|
524
|
+
// coupon the merchant made by hand). Create nothing.
|
|
525
|
+
// 3. none — report it. A dead source setting or a coupon discovery missed. The merchant
|
|
526
|
+
// decides; a migration does not conjure a discount code nobody asked for.
|
|
527
|
+
// `targetCoupons: null` means step 2 has not been run yet — the caller must do that live read
|
|
528
|
+
// before treating a 'none' as final, which is why the un-run state has its own value.
|
|
529
|
+
// A Wix coupon read back from the API nests its fields under `specification` — the query response
|
|
530
|
+
// is `{ coupon: { id, specification: { code, freeShipping, ... } } }`, which wix-writers.js's own
|
|
531
|
+
// post-create lookup already relies on (`coupon?.specification?.code`). Reading the flat shape
|
|
532
|
+
// only meant an existing target coupon came back as coverage 'none' on every re-run.
|
|
533
|
+
// Both shapes are accepted rather than one: a caller may hand over already-unwrapped
|
|
534
|
+
// specifications, and rejecting those would trade one wrong answer for another.
|
|
535
|
+
function couponSpec(coupon) {
|
|
536
|
+
if (!coupon || typeof coupon !== 'object') return {};
|
|
537
|
+
return coupon.specification && typeof coupon.specification === 'object' ? coupon.specification : coupon;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function resolveFreeShippingCouponCoverage({ sourceCoupons = [], targetCoupons = null } = {}) {
|
|
541
|
+
const sourceMatches = (sourceCoupons || []).filter((coupon) => coupon?.free_shipping === true);
|
|
542
|
+
if (sourceMatches.length > 0) {
|
|
543
|
+
return { coverage: 'source', codes: sourceMatches.map((coupon) => coupon.code).filter(Boolean), createCoupon: false };
|
|
544
|
+
}
|
|
545
|
+
if (targetCoupons === null || targetCoupons === undefined) {
|
|
546
|
+
return { coverage: 'unchecked', codes: [], createCoupon: false, reason: 'no free-shipping coupon in the source; query the destination for coupons with freeShipping=true before concluding anything' };
|
|
547
|
+
}
|
|
548
|
+
const targetMatches = (targetCoupons || []).filter((coupon) => couponSpec(coupon).freeShipping === true);
|
|
549
|
+
if (targetMatches.length > 0) {
|
|
550
|
+
return { coverage: 'target', codes: targetMatches.map((coupon) => couponSpec(coupon).code).filter(Boolean), createCoupon: false };
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
coverage: 'none',
|
|
554
|
+
codes: [],
|
|
555
|
+
createCoupon: false,
|
|
556
|
+
reason: 'a coupon-gated free-shipping method exists but no coupon carries the behaviour, in the source or on the destination — either a dead source setting or a coupon the discovery missed. Report for a merchant decision; do not create one.',
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// A source coupon may set the free-shipping flag AND a discount amount at once. Wix's
|
|
561
|
+
// `freeShipping` is its own coupon type and cannot also carry a value, so one half has to go.
|
|
562
|
+
// Decision: keep freeShipping, report the dropped amount with its code and value — the merchant
|
|
563
|
+
// re-creates a plain discount coupon in a minute, but the free-shipping behaviour has no other
|
|
564
|
+
// expression in Wix. Reported with both values so the call is reversible coupon by coupon.
|
|
565
|
+
function findFreeShippingCouponConflicts(sourceCoupons) {
|
|
566
|
+
return (sourceCoupons || [])
|
|
567
|
+
.filter((coupon) => coupon?.free_shipping === true && isNumericString(String(coupon?.amount ?? '')) && Number(coupon.amount) > 0)
|
|
568
|
+
.map((coupon) => ({
|
|
569
|
+
code: coupon.code,
|
|
570
|
+
discountType: coupon.discount_type,
|
|
571
|
+
droppedAmount: String(coupon.amount),
|
|
572
|
+
keeping: 'freeShipping',
|
|
573
|
+
reason: 'Wix coupons cannot carry freeShipping and a discount amount at once. Keeping freeShipping; the amount above is not migrated. Re-create it as a separate discount coupon if the merchant wants both.',
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildDeliveryRegionInput(zone, destinations) {
|
|
578
|
+
if (isBlank(zone?.name)) throw new Error('buildDeliveryRegionInput: zone.name is required');
|
|
579
|
+
return {
|
|
580
|
+
name: String(zone.name).trim(),
|
|
581
|
+
active: true,
|
|
582
|
+
destinations,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function deliveryRegionDedupeKey(destinations) {
|
|
587
|
+
return [...(destinations || [])]
|
|
588
|
+
.map((d) => `${String(d.countryCode || '').toUpperCase()}:${[...(d.subdivisions || [])].sort().join(',')}`)
|
|
589
|
+
.sort()
|
|
590
|
+
.join('|');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Wix's own checkout-blocking warning text (Tax/Delivery Locations UI), verbatim — reused here so
|
|
594
|
+
// the pipeline surfaces the exact same wording at the execution approval gate, not a paraphrase.
|
|
595
|
+
const MISSING_RATES_ALERT = 'This region is missing rates. Add them so customers can complete checkout.';
|
|
596
|
+
|
|
597
|
+
// A zone whose destinations resolve to a real region but which ends up with no way for a buyer
|
|
598
|
+
// to receive the order creates a Wix Delivery Region every matching buyer hits a dead end on.
|
|
599
|
+
// Three kinds count as coverage, and the last two are why this is not simply "has a carrier":
|
|
600
|
+
// - 'carrier' — a delivery choice, obviously.
|
|
601
|
+
// - 'pickup' — collection is a real way to receive an order; it just never becomes a carrier.
|
|
602
|
+
// - 'threshold' — with no paid sibling to fold onto it still yields a conditioned free option.
|
|
603
|
+
// The authoritative signal for the alert is the RESOLVED output (see planShippingZones), because
|
|
604
|
+
// a method can classify as coverage and still be folded away; this predicate stays exported for
|
|
605
|
+
// callers reasoning about a method list they have not resolved yet.
|
|
606
|
+
function hasWorkingCarrier(methodPlans) {
|
|
607
|
+
const covering = new Set(['carrier', 'pickup', 'threshold']);
|
|
608
|
+
return (methodPlans || []).some(({ classified }) => covering.has(classified.kind) && classified.enabled === true);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// --- Option identity ------------------------------------------------------------
|
|
612
|
+
// One Wix option can come from several source methods (a threshold folded onto a paid rate, or
|
|
613
|
+
// two identical rows collapsed), so its identity is the SET of source method instance ids, not a
|
|
614
|
+
// title. Sorted numerically and de-duplicated, so the same zone always yields the same key.
|
|
615
|
+
function sortInstanceIds(ids) {
|
|
616
|
+
const unique = [...new Set((ids || []).filter((id) => id !== undefined && id !== null && id !== '').map(String))];
|
|
617
|
+
return unique.sort((a, b) => {
|
|
618
|
+
const na = Number(a);
|
|
619
|
+
const nb = Number(b);
|
|
620
|
+
if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb) return na - nb;
|
|
621
|
+
return a.localeCompare(b);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function methodInstanceIds(...plans) {
|
|
626
|
+
return sortInstanceIds(plans.filter(Boolean).map(({ method }) => (method ? (method.instance_id ?? method.id) : undefined)));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function shippingOptionKey(zoneId, option) {
|
|
630
|
+
if (zoneId === undefined || zoneId === null || zoneId === '') throw new Error('shippingOptionKey: zoneId is required');
|
|
631
|
+
const ids = sortInstanceIds(option && option.sourceMethodInstanceIds);
|
|
632
|
+
if (ids.length === 0) throw new Error(`shippingOptionKey: option in zone ${zoneId} carries no sourceMethodInstanceIds`);
|
|
633
|
+
return `${zoneId}:${ids.join('+')}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function ratesKey(rates) {
|
|
637
|
+
return JSON.stringify(rates || []);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// --- Resolving a zone's methods to the merchant's actual policy ---------------------------------
|
|
641
|
+
// This is the step that stops the pipeline photocopying the source's own display defect.
|
|
642
|
+
//
|
|
643
|
+
// 1. Enabled 'threshold' methods are removed as rows and folded onto the paid 'carrier' options
|
|
644
|
+
// in the same zone, as an explicit LT/GTE price partition. Several thresholds -> the LOWEST
|
|
645
|
+
// one wins, matching WooCommerce showing free shipping as soon as any threshold is met.
|
|
646
|
+
// 2. A zone with a threshold and no paid option to fold onto keeps the threshold as its own
|
|
647
|
+
// option, carrying the GTE condition. Never as an unconditional 0.
|
|
648
|
+
// 3. Identical survivors (same title, same rates) collapse to one.
|
|
649
|
+
// Every removal is recorded in resolvedSourceDefects[] with what was collapsed and why —
|
|
650
|
+
// a silent collapse is indistinguishable from never having noticed.
|
|
651
|
+
function resolveZoneMethods(methodPlans) {
|
|
652
|
+
const enabledPlans = (methodPlans || []).filter(({ classified }) => classified.enabled === true);
|
|
653
|
+
const thresholds = enabledPlans.filter(({ classified }) => classified.kind === 'threshold');
|
|
654
|
+
const carriers = enabledPlans.filter(({ classified }) => classified.kind === 'carrier');
|
|
655
|
+
const pickups = enabledPlans.filter(({ classified }) => classified.kind === 'pickup');
|
|
656
|
+
const couponDomain = enabledPlans.filter(({ classified }) => classified.kind === 'coupon-domain');
|
|
657
|
+
|
|
658
|
+
const resolvedSourceDefects = [];
|
|
659
|
+
const lowestThresholdPlan = [...thresholds]
|
|
660
|
+
.sort((a, b) => Number(a.classified.thresholdAmount) - Number(b.classified.thresholdAmount))[0];
|
|
661
|
+
const lowestThreshold = lowestThresholdPlan ? lowestThresholdPlan.classified : undefined;
|
|
662
|
+
|
|
663
|
+
const paidCarriers = carriers.filter(({ classified }) => Number(classified.amount) > 0);
|
|
664
|
+
|
|
665
|
+
const optionPlans = carriers.map((plan) => {
|
|
666
|
+
const { classified } = plan;
|
|
667
|
+
const isPaid = Number(classified.amount) > 0;
|
|
668
|
+
if (lowestThreshold && isPaid) {
|
|
669
|
+
return {
|
|
670
|
+
classified,
|
|
671
|
+
// LIVE-VERIFIED 2026-09-16: priceThresholdRates' explicit LT/GTE
|
|
672
|
+
// two-band partition is REJECTED by the Wix dashboard's own rate editor -- it renders
|
|
673
|
+
// as "Rate by price" mode with invalid, unfillable "From"/"Up to" boundary fields (a
|
|
674
|
+
// merchant opening it sees validation errors), even though the API accepts the write
|
|
675
|
+
// and the underlying checkout economics were confirmed correct by
|
|
676
|
+
// verifyShippingEconomics. Switching to an unconditional base rate PLUS a separate
|
|
677
|
+
// GTE-threshold free rate -- relying on the service's own documented "more than one
|
|
678
|
+
// valid rate: return the lowest" tiebreak, exactly what freeAboveThresholdRates is for
|
|
679
|
+
// -- rendered correctly as "Flat rate" mode with the "Offer free shipping when
|
|
680
|
+
// customers purchase a certain amount" checkbox, confirmed live by the user. This is
|
|
681
|
+
// the correct encoding for a paid-carrier-plus-threshold fold; priceThresholdRates
|
|
682
|
+
// itself is kept (still schema-valid, still economically correct) but is no longer
|
|
683
|
+
// used here, and should not be reached for by new code without re-verifying the
|
|
684
|
+
// dashboard editor first.
|
|
685
|
+
rates: [unconditionalRate(classified.amount), ...freeAboveThresholdRates(lowestThreshold.thresholdAmount)],
|
|
686
|
+
notes: [...classified.notes, ...lowestThreshold.notes],
|
|
687
|
+
sourceMethodTitles: [classified.title, lowestThreshold.title],
|
|
688
|
+
sourceMethodInstanceIds: methodInstanceIds(plan, lowestThresholdPlan),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return { classified, rates: [unconditionalRate(classified.amount)], notes: [...classified.notes], sourceMethodTitles: [classified.title], sourceMethodInstanceIds: methodInstanceIds(plan) };
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
if (lowestThreshold) {
|
|
695
|
+
if (paidCarriers.length > 0) {
|
|
696
|
+
resolvedSourceDefects.push({
|
|
697
|
+
kind: 'threshold-folded',
|
|
698
|
+
removedOption: lowestThreshold.title,
|
|
699
|
+
foldedOnto: paidCarriers.map(({ classified }) => classified.title),
|
|
700
|
+
summary: `The source lists "${lowestThreshold.title}" as its own delivery choice, but it is a minimum-order rule, not a choice — WooCommerce shows it alongside the paid option, so buyers see the same delivery offered twice. Migrated as a price band on ${paidCarriers.length === 1 ? 'that option' : 'those options'} instead: full price under ${lowestThreshold.thresholdAmount}, free at ${lowestThreshold.thresholdAmount} and above.`,
|
|
701
|
+
});
|
|
702
|
+
} else {
|
|
703
|
+
optionPlans.push({
|
|
704
|
+
classified: { kind: 'carrier', carrierRole: 'basic', enabled: true, title: lowestThreshold.title, amount: '0', notes: lowestThreshold.notes },
|
|
705
|
+
rates: freeAboveThresholdRates(lowestThreshold.thresholdAmount),
|
|
706
|
+
notes: [...lowestThreshold.notes, 'No paid option in this zone to fold the threshold onto, so it stays a standalone option — conditioned, never unconditional.'],
|
|
707
|
+
sourceMethodTitles: [lowestThreshold.title],
|
|
708
|
+
sourceMethodInstanceIds: methodInstanceIds(lowestThresholdPlan),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
for (const extra of thresholds.map(({ classified }) => classified).filter((c) => c !== lowestThreshold)) {
|
|
712
|
+
resolvedSourceDefects.push({
|
|
713
|
+
kind: 'threshold-superseded',
|
|
714
|
+
removedOption: extra.title,
|
|
715
|
+
summary: `A second minimum-order free-shipping rule at ${extra.thresholdAmount} is superseded by the lower ${lowestThreshold.thresholdAmount} threshold, which is already met first. Not migrated as a separate row.`,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
for (const { classified } of couponDomain) {
|
|
721
|
+
resolvedSourceDefects.push({
|
|
722
|
+
kind: 'coupon-domain-removed',
|
|
723
|
+
removedOption: classified.title,
|
|
724
|
+
summary: `"${classified.title}" is coupon-gated free shipping. In Wix that lives entirely on the coupon — checkout shows the normal delivery price and the coupon removes it — so it is not a delivery setting and produces no shipping row. Nothing is lost.`,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const deduped = [];
|
|
729
|
+
const seen = new Map();
|
|
730
|
+
for (const option of optionPlans) {
|
|
731
|
+
const key = `${option.classified.title}\u0000${ratesKey(option.rates)}`;
|
|
732
|
+
if (seen.has(key)) {
|
|
733
|
+
const kept = seen.get(key);
|
|
734
|
+
kept.sourceMethodTitles.push(...option.sourceMethodTitles);
|
|
735
|
+
kept.sourceMethodInstanceIds = sortInstanceIds([...kept.sourceMethodInstanceIds, ...option.sourceMethodInstanceIds]);
|
|
736
|
+
resolvedSourceDefects.push({
|
|
737
|
+
kind: 'duplicate-collapsed',
|
|
738
|
+
removedOption: option.classified.title,
|
|
739
|
+
summary: `The source has more than one method producing an identical "${option.classified.title}" option at the same price, so buyers see the same delivery choice listed twice. Migrated once.`,
|
|
740
|
+
});
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
seen.set(key, option);
|
|
744
|
+
deduped.push(option);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return { shippingOptions: deduped, pickups, couponDomain, resolvedSourceDefects };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// A zone with empty locations AND empty methods is WooCommerce's unused default "catch-all"
|
|
751
|
+
// (id 0, "Locations not covered by your other zones" on a fresh install) — never migrate it
|
|
752
|
+
// speculatively; only zones the merchant actually configured produce a plan.
|
|
753
|
+
function planShippingZones(zones, { sourceCoupons = [], targetCoupons = null } = {}) {
|
|
754
|
+
const plans = [];
|
|
755
|
+
for (const zone of zones || []) {
|
|
756
|
+
const hasLocations = Array.isArray(zone.locations) && zone.locations.length > 0;
|
|
757
|
+
const hasMethods = Array.isArray(zone.methods) && zone.methods.length > 0;
|
|
758
|
+
if (!hasLocations && !hasMethods) continue;
|
|
759
|
+
|
|
760
|
+
const { destinations, gaps: locationGaps } = normalizeZoneLocations(zone.locations);
|
|
761
|
+
const methodPlans = (zone.methods || []).map((method) => ({ method, classified: classifyMethod(method) }));
|
|
762
|
+
const resolved = resolveZoneMethods(methodPlans);
|
|
763
|
+
|
|
764
|
+
const plan = {
|
|
765
|
+
zone,
|
|
766
|
+
destinations,
|
|
767
|
+
locationGaps,
|
|
768
|
+
methodPlans,
|
|
769
|
+
shippingOptions: resolved.shippingOptions,
|
|
770
|
+
pickups: resolved.pickups,
|
|
771
|
+
resolvedSourceDefects: resolved.resolvedSourceDefects,
|
|
772
|
+
couponCoverage: resolved.couponDomain.length > 0
|
|
773
|
+
? resolveFreeShippingCouponCoverage({ sourceCoupons, targetCoupons })
|
|
774
|
+
: null,
|
|
775
|
+
};
|
|
776
|
+
// Judged on the RESOLVED output, not on the raw method list: a zone can look covered
|
|
777
|
+
// method-by-method and still resolve to nothing a buyer can pick.
|
|
778
|
+
if (destinations.length > 0 && plan.shippingOptions.length === 0 && plan.pickups.length === 0) {
|
|
779
|
+
plan.alert = MISSING_RATES_ALERT;
|
|
780
|
+
}
|
|
781
|
+
plans.push(plan);
|
|
782
|
+
}
|
|
783
|
+
return plans;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function shouldSkipShippingDomain(zones) {
|
|
787
|
+
return planShippingZones(zones).length === 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// --- Verification: check the money, not the row count -------------------------------------------
|
|
791
|
+
// "3 options created" passed while a live store was broken in three different ways at once, so
|
|
792
|
+
// the shipping verification asserts economics against what was actually read back:
|
|
793
|
+
// (a) nothing is free-and-unconditional while something else on the same region costs money —
|
|
794
|
+
// the exact shape of "a dropped condition turned 'free above X' into 'free always'";
|
|
795
|
+
// (b) no shipping option was created for a source pickup method;
|
|
796
|
+
// (c) every carrier's appId matches the role it was classified as;
|
|
797
|
+
// (d) no two options on one region are identical.
|
|
798
|
+
// Returns violations[]; an empty array is the only passing result.
|
|
799
|
+
function verifyShippingEconomics({ regionName, shippingOptions = [], carriers = [], pickupTitles = [], expectedCarrierAppIds = {} } = {}) {
|
|
800
|
+
const violations = [];
|
|
801
|
+
const where = regionName ? ` on region "${regionName}"` : '';
|
|
802
|
+
|
|
803
|
+
const isUnconditionalFree = (option) => (option.rates || []).some((rate) => Number(rate.amount) === 0 && (rate.conditions || []).length === 0);
|
|
804
|
+
const anyPaid = shippingOptions.some((option) => (option.rates || []).some((rate) => Number(rate.amount) > 0));
|
|
805
|
+
if (anyPaid) {
|
|
806
|
+
for (const option of shippingOptions.filter(isUnconditionalFree)) {
|
|
807
|
+
violations.push({
|
|
808
|
+
check: 'unconditional-free-alongside-paid',
|
|
809
|
+
severity: 'blocker',
|
|
810
|
+
summary: `"${option.title}"${where} is free with no condition while another option costs money. Buyers will always pick the free one, so the paid rate is unreachable — this is what a dropped source condition looks like.`,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const pickupSet = new Set(pickupTitles.map((title) => String(title)));
|
|
816
|
+
for (const option of shippingOptions) {
|
|
817
|
+
if (pickupSet.has(String(option.title))) {
|
|
818
|
+
violations.push({
|
|
819
|
+
check: 'shipping-option-for-a-pickup-method',
|
|
820
|
+
severity: 'blocker',
|
|
821
|
+
summary: `"${option.title}"${where} came from a source pickup method but was written as a shipping option. Collection then shows as a 0-priced delivery next to the paid one. It must be a PickupLocation.`,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
for (const carrier of carriers) {
|
|
827
|
+
const expected = expectedCarrierAppIds[carrier.role];
|
|
828
|
+
if (expected && carrier.appId !== expected) {
|
|
829
|
+
violations.push({
|
|
830
|
+
check: 'carrier-app-id-does-not-match-role',
|
|
831
|
+
severity: 'blocker',
|
|
832
|
+
summary: `A carrier classified "${carrier.role}"${where} read back with appId ${carrier.appId}, expected ${expected}.`,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const seen = new Set();
|
|
838
|
+
for (const option of shippingOptions) {
|
|
839
|
+
const key = `${option.title}\u0000${ratesKey(option.rates)}`;
|
|
840
|
+
if (seen.has(key)) {
|
|
841
|
+
violations.push({
|
|
842
|
+
check: 'duplicate-option',
|
|
843
|
+
severity: 'blocker',
|
|
844
|
+
summary: `"${option.title}"${where} exists more than once with identical rates — the same delivery choice is listed twice at checkout.`,
|
|
845
|
+
});
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
seen.add(key);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return violations;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// =================================================================================================
|
|
855
|
+
// The delivery estimate: resolution, verified-write accounting, follow-up items.
|
|
856
|
+
// Everything below is pure except applyTargetMissingDecision/auditShippingEstimates, which READ
|
|
857
|
+
// the destination (GET only) and never write to it.
|
|
858
|
+
// =================================================================================================
|
|
859
|
+
|
|
860
|
+
// The reviewed defaults. Adding a language means adding a reviewed row here first;
|
|
861
|
+
// an unlisted language gets NO estimate and a merchant item, never English.
|
|
862
|
+
const DEFAULT_ESTIMATE_BY_LANGUAGE = Object.freeze({
|
|
863
|
+
en: '5-7 business days',
|
|
864
|
+
he: '5–7 ימי עסקים',
|
|
865
|
+
es: '5-7 días hábiles',
|
|
866
|
+
fr: '5 à 7 jours ouvrés',
|
|
867
|
+
de: '5–7 Werktage',
|
|
868
|
+
it: '5-7 giorni lavorativi',
|
|
869
|
+
pt: '5 a 7 dias úteis',
|
|
870
|
+
nl: '5-7 werkdagen',
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
const SHIPPING_OPTION_ENTITY = 'shipping-option';
|
|
874
|
+
const SHIPPING_OPTION_TARGET_TYPE = 'ecom/shipping-option';
|
|
875
|
+
const SHIPPING_OPTION_OPERATION = 'createShippingOptionVerified';
|
|
876
|
+
const SHIPPING_ESTIMATES_FILE = 'execution/shipping-estimates.json';
|
|
877
|
+
const ORCHESTRATION_STATE_CLI = 'skills/wix-replatform/scripts/orchestration-state.js';
|
|
878
|
+
const SHIPPING_ESTIMATE_AUDIT_CLI = 'skills/wix-replatform/resources/rp-execute-import/scripts/shipping-estimate-audit.js';
|
|
879
|
+
const SHIPPING_COMPLETION_CODES = Object.freeze([
|
|
880
|
+
'shipping-estimate-default',
|
|
881
|
+
'shipping-estimate-missing',
|
|
882
|
+
'shipping-estimate-legacy-blank',
|
|
883
|
+
'shipping-option-reconcile',
|
|
884
|
+
'shipping-option-target-missing',
|
|
885
|
+
]);
|
|
886
|
+
const ACTION_LIST_LIMIT = 15;
|
|
887
|
+
|
|
888
|
+
function textOf(value) {
|
|
889
|
+
if (value === undefined || value === null) return '';
|
|
890
|
+
if (typeof value === 'string') return value.trim();
|
|
891
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
892
|
+
return '';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// The DESTINATION site's language, from a Site Properties read. `null` (a failed read, or nothing
|
|
896
|
+
// set) is treated as an unsupported language by resolveDeliveryEstimate.
|
|
897
|
+
function siteLanguageFromProperties(properties) {
|
|
898
|
+
const raw = textOf(properties && properties.language) || textOf(properties && properties.locale && properties.locale.languageCode);
|
|
899
|
+
return raw ? raw.toLowerCase() : null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function primaryLanguage(siteLanguage) {
|
|
903
|
+
const raw = textOf(siteLanguage).toLowerCase();
|
|
904
|
+
return raw ? raw.split(/[-_]/)[0] : null;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function resolveDeliveryEstimate({ sourceText, merchantText, siteLanguage } = {}) {
|
|
908
|
+
const merchant = textOf(merchantText);
|
|
909
|
+
if (merchant) return { text: merchant, origin: 'merchant' };
|
|
910
|
+
const source = textOf(sourceText);
|
|
911
|
+
if (source) return { text: source, origin: 'source' };
|
|
912
|
+
const language = primaryLanguage(siteLanguage);
|
|
913
|
+
if (language && Object.prototype.hasOwnProperty.call(DEFAULT_ESTIMATE_BY_LANGUAGE, language)) {
|
|
914
|
+
return { text: DEFAULT_ESTIMATE_BY_LANGUAGE[language], origin: 'default' };
|
|
915
|
+
}
|
|
916
|
+
return { text: '', origin: 'missing' };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Only a `source: "user"` decision is the merchant's own words. Anything else is listed, never used.
|
|
920
|
+
function userDecision(decisions, key) {
|
|
921
|
+
const entry = decisions && typeof decisions === 'object' ? decisions[key] : undefined;
|
|
922
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return { entry: null, ignored: null };
|
|
923
|
+
if (entry.source === 'user') return { entry, ignored: null };
|
|
924
|
+
return { entry: null, ignored: { key, source: entry.source === undefined ? null : entry.source } };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function planShippingEstimates({ zonePlans = [], decisions = {}, siteLanguage = null } = {}) {
|
|
928
|
+
const options = [];
|
|
929
|
+
const ignoredDecisions = [];
|
|
930
|
+
const ignoredKeys = new Set();
|
|
931
|
+
for (const plan of zonePlans || []) {
|
|
932
|
+
const zone = plan.zone || {};
|
|
933
|
+
const zoneId = String(zone.id);
|
|
934
|
+
const decisionKey = `shipping-delivery-estimate:${zoneId}`;
|
|
935
|
+
const { entry, ignored } = userDecision(decisions, decisionKey);
|
|
936
|
+
if (ignored && !ignoredKeys.has(decisionKey)) {
|
|
937
|
+
ignoredKeys.add(decisionKey);
|
|
938
|
+
ignoredDecisions.push(ignored);
|
|
939
|
+
}
|
|
940
|
+
const sourceText = textOf(zone.deliveryEstimate);
|
|
941
|
+
const merchantText = entry ? textOf(entry.value) : '';
|
|
942
|
+
for (const option of plan.shippingOptions || []) {
|
|
943
|
+
const { text, origin } = resolveDeliveryEstimate({ sourceText, merchantText, siteLanguage });
|
|
944
|
+
options.push({
|
|
945
|
+
optionKey: shippingOptionKey(zoneId, option),
|
|
946
|
+
zoneId,
|
|
947
|
+
zoneName: textOf(zone.name),
|
|
948
|
+
title: option.classified.title || 'Shipping',
|
|
949
|
+
sourceText,
|
|
950
|
+
merchantText,
|
|
951
|
+
text,
|
|
952
|
+
origin,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return { schemaVersion: 1, siteLanguage: siteLanguage || null, options, ignoredDecisions, legacy: [] };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// --- Read-back verification -----------------------------------------------------
|
|
960
|
+
function canonicalCondition(condition) {
|
|
961
|
+
return JSON.stringify([textOf(condition && condition.type), textOf(condition && condition.operator), Number(condition && condition.value)]);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function canonicalRate(rate) {
|
|
965
|
+
return JSON.stringify([
|
|
966
|
+
Number(rate && rate.amount),
|
|
967
|
+
(Array.isArray(rate && rate.conditions) ? rate.conditions : []).map(canonicalCondition).sort(),
|
|
968
|
+
Boolean(rate && rate.multiplyByQuantity),
|
|
969
|
+
]);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function ratesEqual(sentRates, storedRates) {
|
|
973
|
+
const a = (Array.isArray(sentRates) ? sentRates : []).map(canonicalRate).sort();
|
|
974
|
+
const b = (Array.isArray(storedRates) ? storedRates : []).map(canonicalRate).sort();
|
|
975
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function storedEstimate(option) {
|
|
979
|
+
return typeof (option && option.estimatedDeliveryTime) === 'string' ? option.estimatedDeliveryTime : '';
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function verifyShippingOptionReadBack(sent, stored, { ignoreEstimate = false } = {}) {
|
|
983
|
+
const mismatched = [];
|
|
984
|
+
if (!stored || typeof stored !== 'object') return { ok: false, mismatched: ['title', 'estimatedDeliveryTime', 'rates'] };
|
|
985
|
+
if ((sent && sent.title) !== stored.title) mismatched.push('title');
|
|
986
|
+
if (!ignoreEstimate && storedEstimate(sent) !== storedEstimate(stored)) mismatched.push('estimatedDeliveryTime');
|
|
987
|
+
if (!ratesEqual(sent && sent.rates, stored.rates)) mismatched.push('rates');
|
|
988
|
+
return { ok: mismatched.length === 0, mismatched };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// --- Accounting: one result -> the exact journal/crosswalk rows ------------------
|
|
992
|
+
const OUTCOME_ROWS = {
|
|
993
|
+
'shipping-option-created': { status: 'imported' },
|
|
994
|
+
'shipping-option-unverified': { status: 'succeeded_unverified', errorCode: 'READ_BACK_MISMATCH' },
|
|
995
|
+
'shipping-option-unknown': { status: 'succeeded_unverified', errorCode: 'OUTCOME_UNKNOWN' },
|
|
996
|
+
'shipping-option-ambiguous': { status: 'succeeded_unverified', errorCode: 'AMBIGUOUS_MATCH' },
|
|
997
|
+
'shipping-option-absent': { status: 'failed_retryable', errorCode: 'KNOWN_ABSENT' },
|
|
998
|
+
'shipping-option-not-found': { status: 'failed_terminal', errorCode: 'NOT_FOUND' },
|
|
999
|
+
'shipping-option-not-attempted': { status: 'failed_retryable' },
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
function failedStatusFor(httpStatus) {
|
|
1003
|
+
return httpStatus === 429 || (Number.isInteger(httpStatus) && httpStatus >= 500) ? 'failed_retryable' : 'failed_terminal';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// The message is prose only: every identifier lives in a structured field.
|
|
1007
|
+
function outcomeMessage(result, status) {
|
|
1008
|
+
switch (result.outcome) {
|
|
1009
|
+
case 'shipping-option-unverified':
|
|
1010
|
+
return result.reason || `read-back did not match what was sent (${(result.mismatched || []).join(', ') || 'unknown fields'})`;
|
|
1011
|
+
case 'shipping-option-unknown':
|
|
1012
|
+
return `create outcome unknown (${result.reason || 'no readable reply'}); reconcile by reading the region back, never re-send`;
|
|
1013
|
+
case 'shipping-option-ambiguous':
|
|
1014
|
+
return `several options match after an unknown outcome (${(result.candidateTargetIds || []).length} candidates); a recorded shipping-option-adopt decision is required`;
|
|
1015
|
+
case 'shipping-option-absent':
|
|
1016
|
+
return 'reconciliation found no option this run created; the next normal pass may create it';
|
|
1017
|
+
case 'shipping-option-not-found':
|
|
1018
|
+
return 'the recorded target returned 404; never re-created automatically';
|
|
1019
|
+
case 'shipping-option-not-attempted':
|
|
1020
|
+
return result.reason || 'not attempted';
|
|
1021
|
+
case 'shipping-option-failed':
|
|
1022
|
+
return `create rejected with HTTP ${result.httpStatus === null || result.httpStatus === undefined ? 'unknown' : result.httpStatus}${result.errorCode ? ` (${result.errorCode})` : ''}`;
|
|
1023
|
+
default:
|
|
1024
|
+
return status;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function shippingOptionWriteRecord(result, ctx = {}) {
|
|
1029
|
+
if (!result || typeof result.outcome !== 'string') throw new Error('shippingOptionWriteRecord: result.outcome is required');
|
|
1030
|
+
for (const field of ['attemptId', 'sourceSystem', 'sourceId', 'sourceStableKey', 'writeSpecId', 'runId', 'phase']) {
|
|
1031
|
+
if (!ctx[field] || typeof ctx[field] !== 'string') throw new Error(`shippingOptionWriteRecord: ctx.${field} is required`);
|
|
1032
|
+
}
|
|
1033
|
+
let status;
|
|
1034
|
+
let errorCode = null;
|
|
1035
|
+
if (result.outcome === 'shipping-option-failed') {
|
|
1036
|
+
status = failedStatusFor(result.httpStatus);
|
|
1037
|
+
errorCode = result.errorCode || null;
|
|
1038
|
+
} else if (OUTCOME_ROWS[result.outcome]) {
|
|
1039
|
+
status = OUTCOME_ROWS[result.outcome].status;
|
|
1040
|
+
errorCode = result.errorCode || OUTCOME_ROWS[result.outcome].errorCode || null;
|
|
1041
|
+
} else {
|
|
1042
|
+
throw new Error(`shippingOptionWriteRecord: unknown outcome ${result.outcome}`);
|
|
1043
|
+
}
|
|
1044
|
+
const id = result.id || null;
|
|
1045
|
+
const baseReconcile = ctx.reconcile && typeof ctx.reconcile === 'object' ? ctx.reconcile : null;
|
|
1046
|
+
const candidateTargetIds = Array.isArray(result.candidateTargetIds) && result.candidateTargetIds.length > 0 ? [...result.candidateTargetIds] : null;
|
|
1047
|
+
const reconcile = baseReconcile || candidateTargetIds
|
|
1048
|
+
? { ...(baseReconcile || {}), ...(candidateTargetIds ? { candidateTargetIds } : {}) }
|
|
1049
|
+
: undefined;
|
|
1050
|
+
const attempt = {
|
|
1051
|
+
schemaVersion: 1,
|
|
1052
|
+
attemptId: ctx.attemptId,
|
|
1053
|
+
sourceSystem: ctx.sourceSystem,
|
|
1054
|
+
sourceEntityType: SHIPPING_OPTION_ENTITY,
|
|
1055
|
+
sourceId: ctx.sourceId,
|
|
1056
|
+
sourceStableKey: ctx.sourceStableKey,
|
|
1057
|
+
writeSpecId: ctx.writeSpecId,
|
|
1058
|
+
runId: ctx.runId,
|
|
1059
|
+
phase: ctx.phase,
|
|
1060
|
+
operation: SHIPPING_OPTION_OPERATION,
|
|
1061
|
+
targetEntityType: SHIPPING_OPTION_TARGET_TYPE,
|
|
1062
|
+
status,
|
|
1063
|
+
...(status === 'imported' ? { targetId: id } : {}),
|
|
1064
|
+
...(result.estimateKept ? { estimateKept: result.estimateKept } : {}),
|
|
1065
|
+
...(reconcile ? { reconcile } : {}),
|
|
1066
|
+
updatedAt: ctx.now || new Date().toISOString(),
|
|
1067
|
+
};
|
|
1068
|
+
if (status !== 'imported') {
|
|
1069
|
+
attempt.failure = {
|
|
1070
|
+
sourceId: ctx.sourceId,
|
|
1071
|
+
targetEntityId: id,
|
|
1072
|
+
operation: SHIPPING_OPTION_OPERATION,
|
|
1073
|
+
httpStatus: Number.isInteger(result.httpStatus) ? result.httpStatus : null,
|
|
1074
|
+
errorCode,
|
|
1075
|
+
scopeGroupName: null,
|
|
1076
|
+
outcome: { failed_retryable: 'failed_retryable', failed_terminal: 'failed_terminal', succeeded_unverified: 'unresolved' }[status],
|
|
1077
|
+
...(Array.isArray(result.mismatched) && result.mismatched.length ? { mismatched: [...result.mismatched] } : {}),
|
|
1078
|
+
message: outcomeMessage(result, status),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
const crosswalk = status === 'imported'
|
|
1082
|
+
? {
|
|
1083
|
+
schemaVersion: 1,
|
|
1084
|
+
sourceSystem: ctx.sourceSystem,
|
|
1085
|
+
sourceEntityType: SHIPPING_OPTION_ENTITY,
|
|
1086
|
+
sourceId: ctx.sourceId,
|
|
1087
|
+
sourceStableKey: ctx.sourceStableKey,
|
|
1088
|
+
targetSystem: 'wix',
|
|
1089
|
+
targetEntityType: SHIPPING_OPTION_TARGET_TYPE,
|
|
1090
|
+
targetId: id,
|
|
1091
|
+
status: 'imported',
|
|
1092
|
+
updatedAt: attempt.updatedAt,
|
|
1093
|
+
}
|
|
1094
|
+
: null;
|
|
1095
|
+
return { attempt, crosswalk };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// --- Linked targets: missing-target decisions and the read-only audit --------
|
|
1099
|
+
function isNotFound(error) {
|
|
1100
|
+
return Boolean(error && error.status === 404 && error.outcomeKnown !== false);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function readShippingOption(wix, id) {
|
|
1104
|
+
const { getShippingOption } = require('./wix-writers.js');
|
|
1105
|
+
try {
|
|
1106
|
+
const option = await getShippingOption(wix, id);
|
|
1107
|
+
// A 2xx without the requested option is an unknown read, never evidence: no blank-estimate
|
|
1108
|
+
// conclusion and no crosswalk adoption may follow from it.
|
|
1109
|
+
if (!option || typeof option !== 'object' || Array.isArray(option) || option.id !== id) {
|
|
1110
|
+
return { ok: false, notFound: false, malformed: true };
|
|
1111
|
+
}
|
|
1112
|
+
return { ok: true, option };
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
return { ok: false, notFound: isNotFound(error), error };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// One shared applier for the shipping stage and the finished-project audit, so a recorded
|
|
1119
|
+
// `shipping-option-target-missing:<optionKey>` decision behaves identically in both. It never
|
|
1120
|
+
// writes to Wix; a changed adoption returns ONE replacement crosswalk row for the caller to upsert.
|
|
1121
|
+
async function applyTargetMissingDecision(wix, { crosswalkRow, decisions = {}, now } = {}) {
|
|
1122
|
+
if (!crosswalkRow || !crosswalkRow.sourceId) throw new Error('applyTargetMissingDecision: crosswalkRow is required');
|
|
1123
|
+
const key = `shipping-option-target-missing:${crosswalkRow.sourceId}`;
|
|
1124
|
+
const { entry, ignored } = userDecision(decisions, key);
|
|
1125
|
+
const value = entry ? textOf(entry.value) : '';
|
|
1126
|
+
if (!entry || !value) return { state: 'open', ...(ignored ? { ignoredDecision: ignored } : {}) };
|
|
1127
|
+
if (value === 'retired') return { state: 'retired' };
|
|
1128
|
+
const match = /^recreated:(.+)$/.exec(value);
|
|
1129
|
+
if (!match) return { state: 'open', invalidDecision: { key, value } };
|
|
1130
|
+
const newTargetId = match[1].trim();
|
|
1131
|
+
if (crosswalkRow.targetId === newTargetId && crosswalkRow.provenance === 'operator-recreated') {
|
|
1132
|
+
// Already adopted by an earlier run: no mutation, so a decision that stays in decisions.json
|
|
1133
|
+
// can never grow or rewrite the crosswalk.
|
|
1134
|
+
return { state: 'adopted', auditRow: crosswalkRow };
|
|
1135
|
+
}
|
|
1136
|
+
const read = await readShippingOption(wix, newTargetId);
|
|
1137
|
+
if (!read.ok && read.notFound) return { state: 'recorded-id-missing', newTargetId };
|
|
1138
|
+
if (!read.ok) return { state: 'open', readError: true, newTargetId };
|
|
1139
|
+
const replacement = {
|
|
1140
|
+
...crosswalkRow,
|
|
1141
|
+
targetId: newTargetId,
|
|
1142
|
+
provenance: 'operator-recreated',
|
|
1143
|
+
updatedAt: now || new Date().toISOString(),
|
|
1144
|
+
};
|
|
1145
|
+
delete replacement.verification;
|
|
1146
|
+
return { state: 'adopted', crosswalkUpsert: replacement, auditRow: replacement, option: read.option };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function fixedWriterTargetIds(attemptRows) {
|
|
1150
|
+
const ids = new Set();
|
|
1151
|
+
for (const row of attemptRows || []) {
|
|
1152
|
+
if (row && row.status === 'imported' && row.operation === SHIPPING_OPTION_OPERATION && row.targetId) ids.add(row.targetId);
|
|
1153
|
+
}
|
|
1154
|
+
return ids;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function resolutionByKey(resolution) {
|
|
1158
|
+
const map = new Map();
|
|
1159
|
+
for (const option of (resolution && Array.isArray(resolution.options) ? resolution.options : [])) map.set(option.optionKey, option);
|
|
1160
|
+
return map;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function optionLabel(optionKey, resolution) {
|
|
1164
|
+
const resolved = resolutionByKey(resolution).get(optionKey);
|
|
1165
|
+
return resolved ? `${resolved.zoneName} → ${resolved.title} [${optionKey}]` : `option ${optionKey}`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Read-only. Processes the EFFECTIVE crosswalk (one row per source key), so each source option gets
|
|
1169
|
+
// at most one GET, one result and one contribution per run. Returns what the fragment needs:
|
|
1170
|
+
// readBack — optionKey -> stored option, for targets the fixed writer created (or an
|
|
1171
|
+
// adopted operator-recreated target), feeding -default / -missing;
|
|
1172
|
+
// legacyBlank — earlier-migration options still showing no estimate (report only);
|
|
1173
|
+
// targetMissing — linked targets that 404 (either provenance), with no valid user decision;
|
|
1174
|
+
// crosswalkUpserts — replacement rows for a changed `recreated:<id>` adoption.
|
|
1175
|
+
async function auditShippingEstimates(wix, { crosswalkBySource = {}, attemptRows = [], resolution = null, decisions = {}, now } = {}) {
|
|
1176
|
+
if (Array.isArray(crosswalkBySource)) throw new Error('auditShippingEstimates: pass the effective crosswalk bySource map, never raw crosswalk history');
|
|
1177
|
+
const fixedIds = fixedWriterTargetIds(attemptRows);
|
|
1178
|
+
const resolved = resolutionByKey(resolution);
|
|
1179
|
+
const out = { readBack: {}, recreated: {}, legacyBlank: [], targetMissing: [], crosswalkUpserts: [], retired: [], gets: 0 };
|
|
1180
|
+
const rows = Object.values(crosswalkBySource)
|
|
1181
|
+
.filter((row) => row && row.targetEntityType === SHIPPING_OPTION_TARGET_TYPE)
|
|
1182
|
+
.sort((a, b) => String(a.sourceStableKey).localeCompare(String(b.sourceStableKey)));
|
|
1183
|
+
for (const linked of rows) {
|
|
1184
|
+
const optionKey = linked.sourceId;
|
|
1185
|
+
const label = optionLabel(optionKey, resolution);
|
|
1186
|
+
const decision = await applyTargetMissingDecision(wix, { crosswalkRow: linked, decisions, now });
|
|
1187
|
+
if (decision.state === 'retired') {
|
|
1188
|
+
out.retired.push(optionKey);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (decision.state === 'recorded-id-missing') {
|
|
1192
|
+
out.targetMissing.push({
|
|
1193
|
+
optionKey, targetId: linked.targetId, provenance: fixedIds.has(linked.targetId) ? 'fixed-writer' : 'earlier-migration',
|
|
1194
|
+
label, recordedIdMissing: true, newTargetId: decision.newTargetId,
|
|
1195
|
+
});
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const row = decision.state === 'adopted' ? decision.auditRow : linked;
|
|
1199
|
+
if (decision.crosswalkUpsert) out.crosswalkUpserts.push(decision.crosswalkUpsert);
|
|
1200
|
+
let option = decision.option;
|
|
1201
|
+
if (!option) {
|
|
1202
|
+
out.gets += 1;
|
|
1203
|
+
const read = await readShippingOption(wix, row.targetId);
|
|
1204
|
+
if (!read.ok && read.notFound) {
|
|
1205
|
+
out.targetMissing.push({
|
|
1206
|
+
optionKey, targetId: row.targetId, provenance: fixedIds.has(row.targetId) ? 'fixed-writer' : 'earlier-migration', label,
|
|
1207
|
+
});
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (!read.ok) continue; // unknown read: nothing is concluded, nothing is written
|
|
1211
|
+
option = read.option;
|
|
1212
|
+
} else {
|
|
1213
|
+
out.gets += 1;
|
|
1214
|
+
}
|
|
1215
|
+
if (row.provenance === 'operator-recreated') {
|
|
1216
|
+
out.recreated[optionKey] = option;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (fixedIds.has(row.targetId)) {
|
|
1220
|
+
out.readBack[optionKey] = option;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
// Earlier-migration evidence: report a blank estimate, never edit it.
|
|
1224
|
+
if (storedEstimate(option) === '') {
|
|
1225
|
+
const proposal = resolved.get(optionKey);
|
|
1226
|
+
out.legacyBlank.push({
|
|
1227
|
+
optionKey,
|
|
1228
|
+
targetId: row.targetId,
|
|
1229
|
+
title: option && option.title ? option.title : (proposal ? proposal.title : null),
|
|
1230
|
+
label: proposal ? `${proposal.zoneName} → ${proposal.title}` : `option ${optionKey}`,
|
|
1231
|
+
proposedText: proposal && proposal.text ? proposal.text : null,
|
|
1232
|
+
proposedOrigin: proposal ? proposal.origin : 'unknown',
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return out;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// --- The completion fragment ----------------------------------------------------
|
|
1240
|
+
function shellQuote(value) {
|
|
1241
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function listItems(items) {
|
|
1245
|
+
const shown = items.slice(0, ACTION_LIST_LIMIT);
|
|
1246
|
+
const more = items.length - shown.length;
|
|
1247
|
+
return `${shown.join('; ')}${more > 0 ? `; and ${more} more` : ''}`;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function relayPrompt(code, count, projectDirArg) {
|
|
1251
|
+
if (count <= ACTION_LIST_LIMIT) return '';
|
|
1252
|
+
return ` Then list every option in ${SHIPPING_ESTIMATES_FILE} with code ${code} and send it to the Cutover Operator: \`node ${SHIPPING_ESTIMATE_AUDIT_CLI} --project-dir ${projectDirArg} --list ${code}\`.`;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function actionRow({ code, severity, bmStep, items, prompt, deadlineReason, projectDirArg }) {
|
|
1256
|
+
return {
|
|
1257
|
+
code,
|
|
1258
|
+
severity,
|
|
1259
|
+
action: `${bmStep} Affected: ${listItems(items)}. Requires Import Operator: ${prompt}${relayPrompt(code, items.length, projectDirArg)}`,
|
|
1260
|
+
deadlineReason,
|
|
1261
|
+
entity: SHIPPING_OPTION_ENTITY,
|
|
1262
|
+
affectedCount: items.length,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function rerunSentence(projectDirArg) {
|
|
1267
|
+
return `rerun the shipping stage, or for a finished project \`node ${SHIPPING_ESTIMATE_AUDIT_CLI} --project-dir ${projectDirArg}\``;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function decideCommand(projectDirArg, key, value) {
|
|
1271
|
+
return `\`node ${ORCHESTRATION_STATE_CLI} decide --project-dir ${projectDirArg} --key ${shellQuote(key)} --value ${shellQuote(value)} --source user\``;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Builds the five shipping operatorActions from the CURRENT state, not just this run's events,
|
|
1275
|
+
// and also returns the per-option `items` written to execution/shipping-estimates.json.
|
|
1276
|
+
function shippingEstimateCompletionFragment({ resolution = null, readBack = {}, latestAttempts = {}, audit = null, projectDir = null } = {}) {
|
|
1277
|
+
const projectDirArg = projectDir ? shellQuote(projectDir) : '<projectDir>';
|
|
1278
|
+
const operatorActions = [];
|
|
1279
|
+
const items = [];
|
|
1280
|
+
const resolved = resolution && Array.isArray(resolution.options) ? resolution.options : [];
|
|
1281
|
+
const resolvedByKey = resolutionByKey(resolution);
|
|
1282
|
+
const effectiveAudit = audit || { recreated: {}, legacyBlank: [], targetMissing: [] };
|
|
1283
|
+
|
|
1284
|
+
// -default: the migration's default is still what checkout shows.
|
|
1285
|
+
const defaults = resolved.filter((option) => option.origin === 'default'
|
|
1286
|
+
&& readBack[option.optionKey] && storedEstimate(readBack[option.optionKey]) === option.text);
|
|
1287
|
+
if (defaults.length) {
|
|
1288
|
+
const zones = [...new Map(defaults.map((option) => [option.zoneId, option])).values()];
|
|
1289
|
+
const commands = zones.map((option) => decideCommand(projectDirArg, `shipping-delivery-estimate:${option.zoneId}`, option.text));
|
|
1290
|
+
operatorActions.push(actionRow({
|
|
1291
|
+
code: 'shipping-estimate-default',
|
|
1292
|
+
severity: 'medium',
|
|
1293
|
+
bmStep: 'Business Manager: ask the merchant to confirm the delivery time shown on each listed option, or enter theirs (Settings → Shipping & delivery).',
|
|
1294
|
+
items: defaults.map((option) => `${option.zoneName} → ${option.title} (proposed: "${option.text}")`),
|
|
1295
|
+
prompt: `if the merchant confirms the default unchanged, record it for each listed zone: ${commands.join('; ')}; then ${rerunSentence(projectDirArg)}. A changed value entered in Business Manager clears this item on that rerun.`,
|
|
1296
|
+
deadlineReason: 'Checkout promises a window the store never stated',
|
|
1297
|
+
projectDirArg,
|
|
1298
|
+
}));
|
|
1299
|
+
for (const option of defaults) items.push({ code: 'shipping-estimate-default', optionKey: option.optionKey, label: `${option.zoneName} → ${option.title}`, proposedText: option.text });
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// -missing: a fixed-writer (or hand-recreated) option still blank at checkout.
|
|
1303
|
+
const missing = [];
|
|
1304
|
+
for (const [optionKey, stored] of Object.entries(readBack)) {
|
|
1305
|
+
if (storedEstimate(stored) !== '') continue;
|
|
1306
|
+
const option = resolvedByKey.get(optionKey);
|
|
1307
|
+
missing.push({ optionKey, label: option ? `${option.zoneName} → ${option.title}` : `option ${optionKey}`, proposedText: null });
|
|
1308
|
+
}
|
|
1309
|
+
for (const [optionKey, stored] of Object.entries(effectiveAudit.recreated || {})) {
|
|
1310
|
+
if (storedEstimate(stored) !== '') continue;
|
|
1311
|
+
const option = resolvedByKey.get(optionKey);
|
|
1312
|
+
missing.push({ optionKey, label: `${option ? `${option.zoneName} → ${option.title}` : `option ${optionKey}`} (recreated by hand)`, proposedText: null });
|
|
1313
|
+
}
|
|
1314
|
+
missing.sort((a, b) => a.optionKey.localeCompare(b.optionKey));
|
|
1315
|
+
if (missing.length) {
|
|
1316
|
+
operatorActions.push(actionRow({
|
|
1317
|
+
code: 'shipping-estimate-missing',
|
|
1318
|
+
severity: 'high',
|
|
1319
|
+
bmStep: 'Business Manager: ask the merchant for a delivery time and enter it on each listed option (Settings → Shipping & delivery).',
|
|
1320
|
+
items: missing.map((entry) => entry.label),
|
|
1321
|
+
prompt: `after the delivery times are entered, ${rerunSentence(projectDirArg)} so the read-back clears this item.`,
|
|
1322
|
+
deadlineReason: 'Checkout shows no delivery time',
|
|
1323
|
+
projectDirArg,
|
|
1324
|
+
}));
|
|
1325
|
+
for (const entry of missing) items.push({ code: 'shipping-estimate-missing', ...entry });
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// -reconcile: every latest folded row still AMBIGUOUS_MATCH, until a recorded choice resolves it.
|
|
1329
|
+
const ambiguous = Object.values(latestAttempts || {})
|
|
1330
|
+
.filter((row) => row && row.sourceEntityType === SHIPPING_OPTION_ENTITY && row.status === 'succeeded_unverified'
|
|
1331
|
+
&& row.failure && row.failure.errorCode === 'AMBIGUOUS_MATCH')
|
|
1332
|
+
.sort((a, b) => String(a.sourceId).localeCompare(String(b.sourceId)));
|
|
1333
|
+
if (ambiguous.length) {
|
|
1334
|
+
const commands = ambiguous.map((row) => decideCommand(projectDirArg, `shipping-option-adopt:${row.sourceId}`, '<targetId>'));
|
|
1335
|
+
operatorActions.push(actionRow({
|
|
1336
|
+
code: 'shipping-option-reconcile',
|
|
1337
|
+
severity: 'high',
|
|
1338
|
+
bmStep: 'Business Manager: open the listed candidate options and identify which one, if any, the migration created. Do not delete any yet.',
|
|
1339
|
+
items: ambiguous.map((row) => `${optionLabel(row.sourceId, resolution)} (candidates: ${((row.reconcile && row.reconcile.candidateTargetIds) || []).join(', ')})`),
|
|
1340
|
+
prompt: `record the choice: ${commands.join('; ')} (or --value none when none of them is the migration's), then run the import with --unresolved-only.`,
|
|
1341
|
+
deadlineReason: "A write's outcome is unknown and several options match; nothing more is created until this is recorded",
|
|
1342
|
+
projectDirArg,
|
|
1343
|
+
}));
|
|
1344
|
+
for (const row of ambiguous) items.push({ code: 'shipping-option-reconcile', optionKey: row.sourceId, candidateTargetIds: [...((row.reconcile && row.reconcile.candidateTargetIds) || [])] });
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// -target-missing: a crosswalked option that 404s. Never recreated automatically.
|
|
1348
|
+
const targetMissing = [...(effectiveAudit.targetMissing || [])].sort((a, b) => a.optionKey.localeCompare(b.optionKey));
|
|
1349
|
+
if (targetMissing.length) {
|
|
1350
|
+
const commands = targetMissing.map((entry) => `${decideCommand(projectDirArg, `shipping-option-target-missing:${entry.optionKey}`, 'recreated:<newTargetId>')} (or --value retired)`);
|
|
1351
|
+
operatorActions.push(actionRow({
|
|
1352
|
+
code: 'shipping-option-target-missing',
|
|
1353
|
+
severity: 'high',
|
|
1354
|
+
bmStep: 'Business Manager: these shipping options no longer exist. Ask the merchant whether each should be offered again; if yes, create it in Settings → Shipping & delivery and note its id.',
|
|
1355
|
+
items: targetMissing.map((entry) => `${entry.label} (was ${entry.targetId}, created by ${entry.provenance === 'fixed-writer' ? 'the fixed writer' : 'an earlier migration'})${entry.recordedIdMissing ? ` — the recorded new id was not found (${entry.newTargetId})` : ''}`),
|
|
1356
|
+
prompt: `record the outcome: ${commands.join('; ')}. Then ${rerunSentence(projectDirArg)}.`,
|
|
1357
|
+
deadlineReason: 'A shipping choice the migration created is gone from checkout; the migration will not recreate it',
|
|
1358
|
+
projectDirArg,
|
|
1359
|
+
}));
|
|
1360
|
+
for (const entry of targetMissing) items.push({ code: 'shipping-option-target-missing', optionKey: entry.optionKey, targetId: entry.targetId, provenance: entry.provenance, label: entry.label, ...(entry.recordedIdMissing ? { recordedIdMissing: true } : {}) });
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// -legacy-blank: an earlier migration's option, report only.
|
|
1364
|
+
const legacy = [...(effectiveAudit.legacyBlank || [])].sort((a, b) => a.optionKey.localeCompare(b.optionKey));
|
|
1365
|
+
if (legacy.length) {
|
|
1366
|
+
operatorActions.push(actionRow({
|
|
1367
|
+
code: 'shipping-estimate-legacy-blank',
|
|
1368
|
+
severity: 'medium',
|
|
1369
|
+
bmStep: 'Business Manager: ask the merchant for a delivery time and enter it on each listed option (Settings → Shipping & delivery); the proposed text is listed.',
|
|
1370
|
+
items: legacy.map((entry) => `${entry.label} (proposed: ${entry.proposedText ? `"${entry.proposedText}"` : 'none — ask the merchant'})`),
|
|
1371
|
+
prompt: `after entry, rerun \`node ${SHIPPING_ESTIMATE_AUDIT_CLI} --project-dir ${projectDirArg}\` so the read-back clears this item.`,
|
|
1372
|
+
deadlineReason: 'Created before the field fix; the shopper sees no delivery time',
|
|
1373
|
+
projectDirArg,
|
|
1374
|
+
}));
|
|
1375
|
+
for (const entry of legacy) items.push({ code: 'shipping-estimate-legacy-blank', ...entry });
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
return { operatorActions, items };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Each run REPLACES the previous run's shipping items, so a resolved item drops out on the next run.
|
|
1382
|
+
function mergeShippingEstimateCompletion(completionInput = {}, fragment = { operatorActions: [] }) {
|
|
1383
|
+
const codes = new Set(SHIPPING_COMPLETION_CODES);
|
|
1384
|
+
return {
|
|
1385
|
+
...completionInput,
|
|
1386
|
+
operatorActions: [
|
|
1387
|
+
...(completionInput.operatorActions || []).filter((row) => !codes.has(row && row.code)),
|
|
1388
|
+
...((fragment && fragment.operatorActions) || []),
|
|
1389
|
+
],
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
module.exports = {
|
|
1394
|
+
BASIC_SHIPPING_APP_ID,
|
|
1395
|
+
CONTINENT_COUNTRIES,
|
|
1396
|
+
MISSING_RATES_ALERT,
|
|
1397
|
+
CONDITION_BY_TOTAL_PRICE,
|
|
1398
|
+
normalizeZoneLocations,
|
|
1399
|
+
classifyMethod,
|
|
1400
|
+
hasWorkingCarrier,
|
|
1401
|
+
unconditionalRate,
|
|
1402
|
+
priceThresholdRates,
|
|
1403
|
+
freeAboveThresholdRates,
|
|
1404
|
+
buildDeliveryCarrierInput,
|
|
1405
|
+
buildPickupCarrierInput,
|
|
1406
|
+
buildShippingOptionInput,
|
|
1407
|
+
buildPickupLocationInput,
|
|
1408
|
+
buildDeliveryRegionInput,
|
|
1409
|
+
deliveryRegionDedupeKey,
|
|
1410
|
+
resolveZoneMethods,
|
|
1411
|
+
pickupLocationsFromWpSettings,
|
|
1412
|
+
buildMinimalPickupAddress,
|
|
1413
|
+
resolvePickupAddressSource,
|
|
1414
|
+
resolveFreeShippingCouponCoverage,
|
|
1415
|
+
findFreeShippingCouponConflicts,
|
|
1416
|
+
planShippingZones,
|
|
1417
|
+
shouldSkipShippingDomain,
|
|
1418
|
+
verifyShippingEconomics,
|
|
1419
|
+
// Delivery estimate: resolution, verified write, follow-up items
|
|
1420
|
+
ESTIMATED_DELIVERY_TIME_MAX,
|
|
1421
|
+
DEFAULT_ESTIMATE_BY_LANGUAGE,
|
|
1422
|
+
SHIPPING_COMPLETION_CODES,
|
|
1423
|
+
SHIPPING_ESTIMATES_FILE,
|
|
1424
|
+
SHIPPING_OPTION_OPERATION,
|
|
1425
|
+
SHIPPING_OPTION_TARGET_TYPE,
|
|
1426
|
+
shippingOptionKey,
|
|
1427
|
+
siteLanguageFromProperties,
|
|
1428
|
+
resolveDeliveryEstimate,
|
|
1429
|
+
planShippingEstimates,
|
|
1430
|
+
verifyShippingOptionReadBack,
|
|
1431
|
+
shippingOptionWriteRecord,
|
|
1432
|
+
applyTargetMissingDecision,
|
|
1433
|
+
auditShippingEstimates,
|
|
1434
|
+
shippingEstimateCompletionFragment,
|
|
1435
|
+
mergeShippingEstimateCompletion,
|
|
1436
|
+
shellQuote,
|
|
1437
|
+
};
|