@automattic/plans-grid-next 1.0.2 → 1.0.3
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/CHANGELOG.md +4 -1
- package/dist/cjs/_shared.scss +4 -3
- package/dist/cjs/components/comparison-grid/index.js +99 -92
- package/dist/cjs/components/comparison-grid/index.js.map +1 -1
- package/dist/cjs/components/comparison-grid/style.scss +10 -2
- package/dist/cjs/components/features-grid/client-logo-list/client-list.js +0 -12
- package/dist/cjs/components/features-grid/client-logo-list/client-list.js.map +1 -1
- package/dist/cjs/components/features-grid/index.js +9 -6
- package/dist/cjs/components/features-grid/index.js.map +1 -1
- package/dist/cjs/components/features-grid/plan-features-list.js +10 -3
- package/dist/cjs/components/features-grid/plan-features-list.js.map +1 -1
- package/dist/cjs/components/features-grid/plan-headers.js +2 -2
- package/dist/cjs/components/features-grid/plan-headers.js.map +1 -1
- package/dist/cjs/components/features-grid/plan-tagline.js +1 -1
- package/dist/cjs/components/features-grid/plan-tagline.js.map +1 -1
- package/dist/cjs/components/features-grid/style.scss +107 -19
- package/dist/cjs/components/features-grid/table.js +1 -1
- package/dist/cjs/components/features-grid/table.js.map +1 -1
- package/dist/cjs/components/features.js +43 -4
- package/dist/cjs/components/features.js.map +1 -1
- package/dist/cjs/components/item.js +1 -1
- package/dist/cjs/components/item.js.map +1 -1
- package/dist/cjs/components/plan-button/index.js +5 -3
- package/dist/cjs/components/plan-button/index.js.map +1 -1
- package/dist/cjs/components/plan-button/style.scss +75 -51
- package/dist/cjs/components/plan-div-td-container.js +4 -1
- package/dist/cjs/components/plan-div-td-container.js.map +1 -1
- package/dist/cjs/components/plan-logo.js +6 -3
- package/dist/cjs/components/plan-logo.js.map +1 -1
- package/dist/cjs/components/plan-type-selector/components/interval-type-dropdown.js +12 -1
- package/dist/cjs/components/plan-type-selector/components/interval-type-dropdown.js.map +1 -1
- package/dist/cjs/components/plan-type-selector/hooks/use-max-discount.js +4 -33
- package/dist/cjs/components/plan-type-selector/hooks/use-max-discount.js.map +1 -1
- package/dist/cjs/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js +11 -13
- package/dist/cjs/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js.map +1 -1
- package/dist/cjs/components/plans-2023-tooltip.js +16 -5
- package/dist/cjs/components/plans-2023-tooltip.js.map +1 -1
- package/dist/cjs/components/shared/action-button/index.js +22 -7
- package/dist/cjs/components/shared/action-button/index.js.map +1 -1
- package/dist/cjs/components/shared/action-button/style.scss +4 -0
- package/dist/cjs/components/shared/billing-timeframe/index.js +8 -4
- package/dist/cjs/components/shared/billing-timeframe/index.js.map +1 -1
- package/dist/cjs/components/shared/header-price/index.js +60 -15
- package/dist/cjs/components/shared/header-price/index.js.map +1 -1
- package/dist/cjs/components/shared/header-price/style.scss +9 -3
- package/dist/cjs/components/shared/storage/components/plan-storage.js +2 -2
- package/dist/cjs/components/shared/storage/components/plan-storage.js.map +1 -1
- package/dist/cjs/components/shared/storage/components/storage-dropdown.js +29 -6
- package/dist/cjs/components/shared/storage/components/storage-dropdown.js.map +1 -1
- package/dist/cjs/components/shared/storage/components/storage-feature-label.js +2 -1
- package/dist/cjs/components/shared/storage/components/storage-feature-label.js.map +1 -1
- package/dist/cjs/components/shared/storage/hooks/use-plan-storage.js +2 -0
- package/dist/cjs/components/shared/storage/hooks/use-plan-storage.js.map +1 -1
- package/dist/cjs/fixtures/sites-purchases.js +2 -4
- package/dist/cjs/fixtures/sites-purchases.js.map +1 -1
- package/dist/cjs/grid-context.js +4 -1
- package/dist/cjs/grid-context.js.map +1 -1
- package/dist/cjs/hooks/data-store/get-renewal-pricing-text.js +50 -0
- package/dist/cjs/hooks/data-store/get-renewal-pricing-text.js.map +1 -0
- package/dist/cjs/hooks/data-store/use-grid-plans-for-comparison-grid.js +6 -1
- package/dist/cjs/hooks/data-store/use-grid-plans-for-comparison-grid.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-grid-plans-for-features-grid.js +6 -1
- package/dist/cjs/hooks/data-store/use-grid-plans-for-features-grid.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-grid-plans.js +175 -21
- package/dist/cjs/hooks/data-store/use-grid-plans.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-highlight-labels.js +13 -4
- package/dist/cjs/hooks/data-store/use-highlight-labels.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-plan-billing-description.js +68 -13
- package/dist/cjs/hooks/data-store/use-plan-billing-description.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-plan-features-for-grid-plans.js +76 -2
- package/dist/cjs/hooks/data-store/use-plan-features-for-grid-plans.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js +60 -12
- package/dist/cjs/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js.map +1 -1
- package/dist/cjs/hooks/data-store/use-title-badges.js +19 -0
- package/dist/cjs/hooks/data-store/use-title-badges.js.map +1 -0
- package/dist/cjs/hooks/use-is-large-currency.js +2 -2
- package/dist/cjs/hooks/use-is-large-currency.js.map +1 -1
- package/dist/cjs/hooks/use-visible-grid-plans.js +70 -0
- package/dist/cjs/hooks/use-visible-grid-plans.js.map +1 -0
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/get-plan-features-object.js +15 -2
- package/dist/cjs/lib/get-plan-features-object.js.map +1 -1
- package/dist/cjs/lib/plan-pricing-utils.js +135 -0
- package/dist/cjs/lib/plan-pricing-utils.js.map +1 -0
- package/dist/esm/_shared.scss +4 -3
- package/dist/esm/components/comparison-grid/index.js +100 -93
- package/dist/esm/components/comparison-grid/index.js.map +1 -1
- package/dist/esm/components/comparison-grid/style.scss +10 -2
- package/dist/esm/components/features-grid/client-logo-list/client-list.js +0 -12
- package/dist/esm/components/features-grid/client-logo-list/client-list.js.map +1 -1
- package/dist/esm/components/features-grid/index.js +9 -6
- package/dist/esm/components/features-grid/index.js.map +1 -1
- package/dist/esm/components/features-grid/plan-features-list.js +10 -3
- package/dist/esm/components/features-grid/plan-features-list.js.map +1 -1
- package/dist/esm/components/features-grid/plan-headers.js +3 -3
- package/dist/esm/components/features-grid/plan-headers.js.map +1 -1
- package/dist/esm/components/features-grid/plan-tagline.js +1 -1
- package/dist/esm/components/features-grid/plan-tagline.js.map +1 -1
- package/dist/esm/components/features-grid/style.scss +107 -19
- package/dist/esm/components/features-grid/table.js +1 -1
- package/dist/esm/components/features-grid/table.js.map +1 -1
- package/dist/esm/components/features.js +44 -5
- package/dist/esm/components/features.js.map +1 -1
- package/dist/esm/components/item.js +1 -1
- package/dist/esm/components/item.js.map +1 -1
- package/dist/esm/components/plan-button/index.js +5 -3
- package/dist/esm/components/plan-button/index.js.map +1 -1
- package/dist/esm/components/plan-button/style.scss +75 -51
- package/dist/esm/components/plan-div-td-container.js +4 -1
- package/dist/esm/components/plan-div-td-container.js.map +1 -1
- package/dist/esm/components/plan-logo.js +7 -4
- package/dist/esm/components/plan-logo.js.map +1 -1
- package/dist/esm/components/plan-type-selector/components/interval-type-dropdown.js +12 -1
- package/dist/esm/components/plan-type-selector/components/interval-type-dropdown.js.map +1 -1
- package/dist/esm/components/plan-type-selector/hooks/use-max-discount.js +3 -33
- package/dist/esm/components/plan-type-selector/hooks/use-max-discount.js.map +1 -1
- package/dist/esm/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js +11 -13
- package/dist/esm/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js.map +1 -1
- package/dist/esm/components/plans-2023-tooltip.js +16 -5
- package/dist/esm/components/plans-2023-tooltip.js.map +1 -1
- package/dist/esm/components/shared/action-button/index.js +22 -7
- package/dist/esm/components/shared/action-button/index.js.map +1 -1
- package/dist/esm/components/shared/action-button/style.scss +4 -0
- package/dist/esm/components/shared/billing-timeframe/index.js +8 -4
- package/dist/esm/components/shared/billing-timeframe/index.js.map +1 -1
- package/dist/esm/components/shared/header-price/index.js +60 -15
- package/dist/esm/components/shared/header-price/index.js.map +1 -1
- package/dist/esm/components/shared/header-price/style.scss +9 -3
- package/dist/esm/components/shared/storage/components/plan-storage.js +2 -2
- package/dist/esm/components/shared/storage/components/plan-storage.js.map +1 -1
- package/dist/esm/components/shared/storage/components/storage-dropdown.js +30 -7
- package/dist/esm/components/shared/storage/components/storage-dropdown.js.map +1 -1
- package/dist/esm/components/shared/storage/components/storage-feature-label.js +2 -1
- package/dist/esm/components/shared/storage/components/storage-feature-label.js.map +1 -1
- package/dist/esm/components/shared/storage/hooks/use-plan-storage.js +3 -1
- package/dist/esm/components/shared/storage/hooks/use-plan-storage.js.map +1 -1
- package/dist/esm/fixtures/sites-purchases.js +2 -4
- package/dist/esm/fixtures/sites-purchases.js.map +1 -1
- package/dist/esm/grid-context.js +4 -1
- package/dist/esm/grid-context.js.map +1 -1
- package/dist/esm/hooks/data-store/get-renewal-pricing-text.js +47 -0
- package/dist/esm/hooks/data-store/get-renewal-pricing-text.js.map +1 -0
- package/dist/esm/hooks/data-store/use-grid-plans-for-comparison-grid.js +6 -1
- package/dist/esm/hooks/data-store/use-grid-plans-for-comparison-grid.js.map +1 -1
- package/dist/esm/hooks/data-store/use-grid-plans-for-features-grid.js +6 -1
- package/dist/esm/hooks/data-store/use-grid-plans-for-features-grid.js.map +1 -1
- package/dist/esm/hooks/data-store/use-grid-plans.js +176 -22
- package/dist/esm/hooks/data-store/use-grid-plans.js.map +1 -1
- package/dist/esm/hooks/data-store/use-highlight-labels.js +14 -5
- package/dist/esm/hooks/data-store/use-highlight-labels.js.map +1 -1
- package/dist/esm/hooks/data-store/use-plan-billing-description.js +66 -11
- package/dist/esm/hooks/data-store/use-plan-billing-description.js.map +1 -1
- package/dist/esm/hooks/data-store/use-plan-features-for-grid-plans.js +77 -3
- package/dist/esm/hooks/data-store/use-plan-features-for-grid-plans.js.map +1 -1
- package/dist/esm/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js +59 -11
- package/dist/esm/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js.map +1 -1
- package/dist/esm/hooks/data-store/use-title-badges.js +17 -0
- package/dist/esm/hooks/data-store/use-title-badges.js.map +1 -0
- package/dist/esm/hooks/use-is-large-currency.js +1 -1
- package/dist/esm/hooks/use-is-large-currency.js.map +1 -1
- package/dist/esm/hooks/use-visible-grid-plans.js +66 -0
- package/dist/esm/hooks/use-visible-grid-plans.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/get-plan-features-object.js +15 -2
- package/dist/esm/lib/get-plan-features-object.js.map +1 -1
- package/dist/esm/lib/plan-pricing-utils.js +129 -0
- package/dist/esm/lib/plan-pricing-utils.js.map +1 -0
- package/dist/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/components/comparison-grid/index.d.ts +1 -1
- package/dist/types/components/comparison-grid/index.d.ts.map +1 -1
- package/dist/types/components/features-grid/client-logo-list/client-list.d.ts.map +1 -1
- package/dist/types/components/features-grid/index.d.ts.map +1 -1
- package/dist/types/components/features-grid/plan-features-list.d.ts.map +1 -1
- package/dist/types/components/features-grid/plan-headers.d.ts +2 -0
- package/dist/types/components/features-grid/plan-headers.d.ts.map +1 -1
- package/dist/types/components/features-grid/table.d.ts.map +1 -1
- package/dist/types/components/features.d.ts.map +1 -1
- package/dist/types/components/item.d.ts +2 -1
- package/dist/types/components/item.d.ts.map +1 -1
- package/dist/types/components/plan-button/index.d.ts +2 -1
- package/dist/types/components/plan-button/index.d.ts.map +1 -1
- package/dist/types/components/plan-div-td-container.d.ts +2 -0
- package/dist/types/components/plan-div-td-container.d.ts.map +1 -1
- package/dist/types/components/plan-logo.d.ts.map +1 -1
- package/dist/types/components/plan-type-selector/components/interval-type-dropdown.d.ts.map +1 -1
- package/dist/types/components/plan-type-selector/hooks/use-max-discount.d.ts.map +1 -1
- package/dist/types/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.d.ts.map +1 -1
- package/dist/types/components/plans-2023-tooltip.d.ts.map +1 -1
- package/dist/types/components/shared/action-button/index.d.ts +2 -1
- package/dist/types/components/shared/action-button/index.d.ts.map +1 -1
- package/dist/types/components/shared/billing-timeframe/index.d.ts.map +1 -1
- package/dist/types/components/shared/header-price/index.d.ts.map +1 -1
- package/dist/types/components/shared/storage/components/storage-dropdown.d.ts.map +1 -1
- package/dist/types/components/shared/storage/components/storage-feature-label.d.ts.map +1 -1
- package/dist/types/components/shared/storage/hooks/use-plan-storage.d.ts +1 -1
- package/dist/types/components/shared/storage/hooks/use-plan-storage.d.ts.map +1 -1
- package/dist/types/fixtures/sites-purchases.d.ts +2 -4
- package/dist/types/fixtures/sites-purchases.d.ts.map +1 -1
- package/dist/types/grid-context.d.ts +4 -1
- package/dist/types/grid-context.d.ts.map +1 -1
- package/dist/types/hooks/data-store/get-renewal-pricing-text.d.ts +14 -0
- package/dist/types/hooks/data-store/get-renewal-pricing-text.d.ts.map +1 -0
- package/dist/types/hooks/data-store/types.d.ts +21 -0
- package/dist/types/hooks/data-store/types.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-grid-plans-for-comparison-grid.d.ts +1 -1
- package/dist/types/hooks/data-store/use-grid-plans-for-comparison-grid.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-grid-plans-for-features-grid.d.ts +1 -1
- package/dist/types/hooks/data-store/use-grid-plans-for-features-grid.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-grid-plans.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-highlight-labels.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-plan-billing-description.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-plan-features-for-grid-plans.d.ts +4 -1
- package/dist/types/hooks/data-store/use-plan-features-for-grid-plans.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-restructured-plan-features-for-comparison-grid.d.ts +4 -1
- package/dist/types/hooks/data-store/use-restructured-plan-features-for-comparison-grid.d.ts.map +1 -1
- package/dist/types/hooks/data-store/use-title-badges.d.ts +9 -0
- package/dist/types/hooks/data-store/use-title-badges.d.ts.map +1 -0
- package/dist/types/hooks/use-visible-grid-plans.d.ts +14 -0
- package/dist/types/hooks/use-visible-grid-plans.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/get-plan-features-object.d.ts +1 -1
- package/dist/types/lib/get-plan-features-object.d.ts.map +1 -1
- package/dist/types/lib/plan-pricing-utils.d.ts +105 -0
- package/dist/types/lib/plan-pricing-utils.d.ts.map +1 -0
- package/dist/types/types.d.ts +29 -2
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +38 -28
- package/src/_shared.scss +4 -3
- package/src/components/comparison-grid/index.tsx +258 -181
- package/src/components/comparison-grid/style.scss +10 -2
- package/src/components/features-grid/client-logo-list/client-list.tsx +0 -25
- package/src/components/features-grid/index.tsx +35 -18
- package/src/components/features-grid/plan-features-list.tsx +15 -4
- package/src/components/features-grid/plan-headers.tsx +10 -3
- package/src/components/features-grid/plan-tagline.tsx +1 -1
- package/src/components/features-grid/style.scss +107 -19
- package/src/components/features-grid/table.tsx +4 -2
- package/src/components/features.tsx +66 -6
- package/src/components/item.tsx +6 -3
- package/src/components/plan-button/index.tsx +7 -1
- package/src/components/plan-button/style.scss +75 -51
- package/src/components/plan-div-td-container.tsx +6 -2
- package/src/components/plan-logo.tsx +16 -9
- package/src/components/plan-type-selector/components/interval-type-dropdown.tsx +14 -1
- package/src/components/plan-type-selector/hooks/use-max-discount.ts +8 -47
- package/src/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.ts +19 -17
- package/src/components/plans-2023-tooltip.tsx +17 -5
- package/src/components/shared/action-button/index.tsx +46 -5
- package/src/components/shared/action-button/style.scss +4 -0
- package/src/components/shared/billing-timeframe/index.tsx +12 -7
- package/src/components/shared/header-price/index.tsx +129 -27
- package/src/components/shared/header-price/style.scss +9 -3
- package/src/components/shared/storage/components/plan-storage.tsx +2 -2
- package/src/components/shared/storage/components/storage-dropdown.tsx +36 -15
- package/src/components/shared/storage/components/storage-feature-label.tsx +2 -1
- package/src/components/shared/storage/hooks/use-plan-storage.ts +3 -0
- package/src/components/test/actions-button.tsx +5 -0
- package/src/components/test/billing-timeframe.tsx +1 -1
- package/src/components/test/header-price.tsx +342 -4
- package/src/fixtures/sites-purchases.ts +2 -4
- package/src/grid-context.tsx +9 -0
- package/src/hooks/data-store/get-renewal-pricing-text.ts +73 -0
- package/src/hooks/data-store/types.ts +21 -0
- package/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts +10 -0
- package/src/hooks/data-store/use-grid-plans-for-features-grid.ts +10 -0
- package/src/hooks/data-store/use-grid-plans.tsx +189 -23
- package/src/hooks/data-store/use-highlight-labels.ts +12 -3
- package/src/hooks/data-store/use-plan-billing-description.tsx +80 -15
- package/src/hooks/data-store/use-plan-features-for-grid-plans.ts +135 -1
- package/src/hooks/data-store/use-restructured-plan-features-for-comparison-grid.ts +93 -20
- package/src/hooks/data-store/use-title-badges.ts +31 -0
- package/src/hooks/test/use-visible-grid-plans.tsx +116 -0
- package/src/hooks/use-is-large-currency.ts +1 -1
- package/src/hooks/use-visible-grid-plans.tsx +102 -0
- package/src/index.tsx +18 -0
- package/src/lib/get-plan-features-object.ts +23 -2
- package/src/lib/plan-pricing-utils.ts +211 -0
- package/src/lib/test/plan-pricing-utils.ts +594 -0
- package/src/style-imports.d.ts +3 -0
- package/src/types.ts +41 -0
- package/dist/cjs/components/features-grid/mobile-free-domain.js +0 -25
- package/dist/cjs/components/features-grid/mobile-free-domain.js.map +0 -1
- package/dist/cjs/lib/get-plan-pricing-info-from-grid-plans.js +0 -15
- package/dist/cjs/lib/get-plan-pricing-info-from-grid-plans.js.map +0 -1
- package/dist/esm/components/features-grid/mobile-free-domain.js +0 -23
- package/dist/esm/components/features-grid/mobile-free-domain.js.map +0 -1
- package/dist/esm/lib/get-plan-pricing-info-from-grid-plans.js +0 -12
- package/dist/esm/lib/get-plan-pricing-info-from-grid-plans.js.map +0 -1
- package/dist/types/components/features-grid/mobile-free-domain.d.ts +0 -8
- package/dist/types/components/features-grid/mobile-free-domain.d.ts.map +0 -1
- package/dist/types/lib/get-plan-pricing-info-from-grid-plans.d.ts +0 -9
- package/dist/types/lib/get-plan-pricing-info-from-grid-plans.d.ts.map +0 -1
- package/src/components/features-grid/mobile-free-domain.tsx +0 -51
- package/src/lib/get-plan-pricing-info-from-grid-plans.ts +0 -31
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPlanPriceForDuration,
|
|
3
|
+
calculateDiscountPercentage,
|
|
4
|
+
fromPricingMetaForGridPlan,
|
|
5
|
+
fromVariantPriceData,
|
|
6
|
+
} from '../plan-pricing-utils';
|
|
7
|
+
import type { PlanPriceInfo, VariantPriceData } from '../plan-pricing-utils';
|
|
8
|
+
import type { Plans } from '@automattic/data-stores';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function makePlanPriceInfo( overrides: Partial< PlanPriceInfo > = {} ): PlanPriceInfo {
|
|
15
|
+
return {
|
|
16
|
+
termMonths: 12,
|
|
17
|
+
regularPricePerMonth: 2000,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makePricingMeta(
|
|
23
|
+
overrides: Partial< Plans.PricingMetaForGridPlan > = {}
|
|
24
|
+
): Plans.PricingMetaForGridPlan {
|
|
25
|
+
return {
|
|
26
|
+
originalPrice: { monthly: 2000, full: 24000 },
|
|
27
|
+
discountedPrice: { monthly: null, full: null },
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeVariant( overrides: Partial< VariantPriceData > = {} ): VariantPriceData {
|
|
33
|
+
return {
|
|
34
|
+
termIntervalInMonths: 12,
|
|
35
|
+
priceInteger: 24000,
|
|
36
|
+
priceBeforeDiscounts: 24000,
|
|
37
|
+
introductoryInterval: 0,
|
|
38
|
+
introductoryTerm: 'year',
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// getPlanPriceForDuration
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe( 'getPlanPriceForDuration', () => {
|
|
48
|
+
describe( 'no intro offer', () => {
|
|
49
|
+
it( 'returns regularPricePerMonth × durationMonths for a monthly plan', () => {
|
|
50
|
+
const info = makePlanPriceInfo( { termMonths: 1, regularPricePerMonth: 2000 } );
|
|
51
|
+
expect( getPlanPriceForDuration( info, 6 ) ).toBe( 12000 );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'returns pro-rated price for a yearly plan over 6 months', () => {
|
|
55
|
+
const info = makePlanPriceInfo( { termMonths: 12, regularPricePerMonth: 1500 } );
|
|
56
|
+
expect( getPlanPriceForDuration( info, 6 ) ).toBe( 9000 );
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
it( 'uses discountedPricePerMonth instead of regularPricePerMonth when provided', () => {
|
|
60
|
+
const info = makePlanPriceInfo( {
|
|
61
|
+
termMonths: 12,
|
|
62
|
+
regularPricePerMonth: 2000,
|
|
63
|
+
discountedPricePerMonth: 1800,
|
|
64
|
+
} );
|
|
65
|
+
expect( getPlanPriceForDuration( info, 6 ) ).toBe( 10800 );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'uses discountedPricePerMonth for post-intro months when both are present', () => {
|
|
69
|
+
const info = makePlanPriceInfo( {
|
|
70
|
+
termMonths: 12,
|
|
71
|
+
regularPricePerMonth: 2000,
|
|
72
|
+
discountedPricePerMonth: 1800,
|
|
73
|
+
introOffer: { pricePerMonth: 1000, durationMonths: 6, isActive: true },
|
|
74
|
+
} );
|
|
75
|
+
// 6 intro months × 1000 + 6 regular months × 1800
|
|
76
|
+
expect( getPlanPriceForDuration( info, 12 ) ).toBe( 6000 + 10800 );
|
|
77
|
+
} );
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
describe( 'with active intro offer', () => {
|
|
81
|
+
const monthlyWithIntro = makePlanPriceInfo( {
|
|
82
|
+
termMonths: 1,
|
|
83
|
+
regularPricePerMonth: 2000,
|
|
84
|
+
introOffer: { pricePerMonth: 1000, durationMonths: 1, isActive: true },
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
const yearlyWithIntro = makePlanPriceInfo( {
|
|
88
|
+
termMonths: 12,
|
|
89
|
+
regularPricePerMonth: 2000,
|
|
90
|
+
introOffer: { pricePerMonth: 1000, durationMonths: 12, isActive: true },
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
it( 'applies intro price for the first N months and regular price thereafter (monthly plan, 6 months)', () => {
|
|
94
|
+
// 1 intro month × 1000 + 5 regular months × 2000
|
|
95
|
+
expect( getPlanPriceForDuration( monthlyWithIntro, 6 ) ).toBe( 11000 );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
it( 'ignores intro price when useIntroOffer is false (monthly plan)', () => {
|
|
99
|
+
expect( getPlanPriceForDuration( monthlyWithIntro, 6, { useIntroOffer: false } ) ).toBe(
|
|
100
|
+
12000
|
|
101
|
+
);
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
it( 'applies intro price for a yearly plan over 6 months (duration shorter than intro)', () => {
|
|
105
|
+
// intro covers 12 months; for 6 months only intro applies
|
|
106
|
+
// 6 × 1000 = 6000
|
|
107
|
+
expect( getPlanPriceForDuration( yearlyWithIntro, 6 ) ).toBe( 6000 );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'ignores intro price when useIntroOffer is false (yearly plan)', () => {
|
|
111
|
+
expect( getPlanPriceForDuration( yearlyWithIntro, 6, { useIntroOffer: false } ) ).toBe(
|
|
112
|
+
12000
|
|
113
|
+
);
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
it( 'handles duration longer than the intro period (3-month duration, 1-month intro)', () => {
|
|
117
|
+
// 1 intro month × 1000 + 2 regular months × 2000
|
|
118
|
+
expect( getPlanPriceForDuration( monthlyWithIntro, 3 ) ).toBe( 5000 );
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
it( 'handles duration exactly equal to the intro period', () => {
|
|
122
|
+
expect( getPlanPriceForDuration( monthlyWithIntro, 1 ) ).toBe( 1000 );
|
|
123
|
+
} );
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
describe( 'with inactive intro offer', () => {
|
|
127
|
+
it( 'falls through to regular price when isActive is false, even if useIntroOffer is true', () => {
|
|
128
|
+
const info = makePlanPriceInfo( {
|
|
129
|
+
termMonths: 12,
|
|
130
|
+
regularPricePerMonth: 2000,
|
|
131
|
+
introOffer: { pricePerMonth: 1000, durationMonths: 12, isActive: false },
|
|
132
|
+
} );
|
|
133
|
+
expect( getPlanPriceForDuration( info, 6 ) ).toBe( 12000 );
|
|
134
|
+
} );
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
describe( 'edge cases', () => {
|
|
138
|
+
it( 'returns 0 for 0 duration months', () => {
|
|
139
|
+
const info = makePlanPriceInfo( { termMonths: 12, regularPricePerMonth: 2000 } );
|
|
140
|
+
expect( getPlanPriceForDuration( info, 0 ) ).toBe( 0 );
|
|
141
|
+
} );
|
|
142
|
+
} );
|
|
143
|
+
} );
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// calculateDiscountPercentage
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
describe( 'calculateDiscountPercentage', () => {
|
|
150
|
+
it( 'returns the floored percentage discount for a standard case', () => {
|
|
151
|
+
// (2000 - 1400) / 2000 × 100 = 30
|
|
152
|
+
expect( calculateDiscountPercentage( 2000, 1400 ) ).toBe( 30 );
|
|
153
|
+
} );
|
|
154
|
+
|
|
155
|
+
it( 'floors fractional percentages (does not round up)', () => {
|
|
156
|
+
// (3000 - 2050) / 3000 × 100 = 31.666... → floor = 31
|
|
157
|
+
expect( calculateDiscountPercentage( 3000, 2050 ) ).toBe( 31 );
|
|
158
|
+
} );
|
|
159
|
+
|
|
160
|
+
it( 'returns undefined when cheaperPrice equals referencePrice (no saving)', () => {
|
|
161
|
+
expect( calculateDiscountPercentage( 2000, 2000 ) ).toBeUndefined();
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
it( 'returns undefined when cheaperPrice is higher than referencePrice', () => {
|
|
165
|
+
expect( calculateDiscountPercentage( 2000, 2500 ) ).toBeUndefined();
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
it( 'returns undefined when referencePrice is zero', () => {
|
|
169
|
+
expect( calculateDiscountPercentage( 0, 0 ) ).toBeUndefined();
|
|
170
|
+
} );
|
|
171
|
+
|
|
172
|
+
it( 'returns undefined when referencePrice is negative', () => {
|
|
173
|
+
expect( calculateDiscountPercentage( -100, -200 ) ).toBeUndefined();
|
|
174
|
+
} );
|
|
175
|
+
|
|
176
|
+
it( 'returns 99 for a near-complete discount (not 100)', () => {
|
|
177
|
+
// (1000 - 1) / 1000 × 100 = 99.9 → floor = 99
|
|
178
|
+
expect( calculateDiscountPercentage( 1000, 1 ) ).toBe( 99 );
|
|
179
|
+
} );
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// fromPricingMetaForGridPlan
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe( 'fromPricingMetaForGridPlan', () => {
|
|
187
|
+
it( 'converts a monthly plan with no intro offer', () => {
|
|
188
|
+
const meta = makePricingMeta( { billingPeriod: 31 } );
|
|
189
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
190
|
+
expect( result ).toEqual( {
|
|
191
|
+
termMonths: 1,
|
|
192
|
+
regularPricePerMonth: 2000,
|
|
193
|
+
discountedPricePerMonth: undefined,
|
|
194
|
+
introOffer: undefined,
|
|
195
|
+
} );
|
|
196
|
+
} );
|
|
197
|
+
|
|
198
|
+
it( 'converts a yearly plan with no intro offer', () => {
|
|
199
|
+
const meta = makePricingMeta( { billingPeriod: 365 } );
|
|
200
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
201
|
+
expect( result ).toEqual( {
|
|
202
|
+
termMonths: 12,
|
|
203
|
+
regularPricePerMonth: 2000,
|
|
204
|
+
discountedPricePerMonth: undefined,
|
|
205
|
+
introOffer: undefined,
|
|
206
|
+
} );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'converts a biennial plan (730 days → 24 months)', () => {
|
|
210
|
+
const meta = makePricingMeta( {
|
|
211
|
+
billingPeriod: 730,
|
|
212
|
+
originalPrice: { monthly: 1500, full: 36000 },
|
|
213
|
+
} );
|
|
214
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
215
|
+
expect( result?.termMonths ).toBe( 24 );
|
|
216
|
+
expect( result?.regularPricePerMonth ).toBe( 1500 );
|
|
217
|
+
} );
|
|
218
|
+
|
|
219
|
+
it( 'converts a triennial plan (1095 days → 36 months)', () => {
|
|
220
|
+
const meta = makePricingMeta( {
|
|
221
|
+
billingPeriod: 1095,
|
|
222
|
+
originalPrice: { monthly: 1200, full: 43200 },
|
|
223
|
+
} );
|
|
224
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
225
|
+
expect( result?.termMonths ).toBe( 36 );
|
|
226
|
+
} );
|
|
227
|
+
|
|
228
|
+
it( 'sets discountedPricePerMonth when discountedPrice.monthly is provided', () => {
|
|
229
|
+
const meta = makePricingMeta( {
|
|
230
|
+
billingPeriod: 365,
|
|
231
|
+
discountedPrice: { monthly: 1800, full: 21600 },
|
|
232
|
+
} );
|
|
233
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
234
|
+
expect( result?.discountedPricePerMonth ).toBe( 1800 );
|
|
235
|
+
} );
|
|
236
|
+
|
|
237
|
+
it( 'leaves discountedPricePerMonth undefined when discountedPrice.monthly is null', () => {
|
|
238
|
+
const meta = makePricingMeta( { billingPeriod: 365 } );
|
|
239
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
240
|
+
expect( result?.discountedPricePerMonth ).toBeUndefined();
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
it( 'maps an active yearly intro offer (intervalUnit: year, intervalCount: 1)', () => {
|
|
244
|
+
const meta = makePricingMeta( {
|
|
245
|
+
billingPeriod: 365,
|
|
246
|
+
introOffer: {
|
|
247
|
+
rawPrice: { monthly: 1000, full: 12000 },
|
|
248
|
+
intervalUnit: 'year',
|
|
249
|
+
intervalCount: 1,
|
|
250
|
+
isOfferComplete: false,
|
|
251
|
+
formattedPrice: '$120',
|
|
252
|
+
},
|
|
253
|
+
} );
|
|
254
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
255
|
+
expect( result?.introOffer ).toEqual( {
|
|
256
|
+
pricePerMonth: 1000,
|
|
257
|
+
durationMonths: 12,
|
|
258
|
+
isActive: true,
|
|
259
|
+
} );
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
it( 'maps a completed intro offer with isActive: false', () => {
|
|
263
|
+
const meta = makePricingMeta( {
|
|
264
|
+
billingPeriod: 365,
|
|
265
|
+
introOffer: {
|
|
266
|
+
rawPrice: { monthly: 1000, full: 12000 },
|
|
267
|
+
intervalUnit: 'year',
|
|
268
|
+
intervalCount: 1,
|
|
269
|
+
isOfferComplete: true,
|
|
270
|
+
formattedPrice: '$120',
|
|
271
|
+
},
|
|
272
|
+
} );
|
|
273
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
274
|
+
expect( result?.introOffer?.isActive ).toBe( false );
|
|
275
|
+
} );
|
|
276
|
+
|
|
277
|
+
it( 'maps an intro offer spanning 2 years (intervalUnit: year, intervalCount: 2 → 24 months)', () => {
|
|
278
|
+
const meta = makePricingMeta( {
|
|
279
|
+
billingPeriod: 730,
|
|
280
|
+
originalPrice: { monthly: 1500, full: 36000 },
|
|
281
|
+
introOffer: {
|
|
282
|
+
rawPrice: { monthly: 900, full: 21600 },
|
|
283
|
+
intervalUnit: 'year',
|
|
284
|
+
intervalCount: 2,
|
|
285
|
+
isOfferComplete: false,
|
|
286
|
+
formattedPrice: '$216',
|
|
287
|
+
},
|
|
288
|
+
} );
|
|
289
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
290
|
+
expect( result?.introOffer?.durationMonths ).toBe( 24 );
|
|
291
|
+
} );
|
|
292
|
+
|
|
293
|
+
it( 'maps an intro offer in months (intervalUnit: month, intervalCount: 3 → 3 months)', () => {
|
|
294
|
+
const meta = makePricingMeta( {
|
|
295
|
+
billingPeriod: 365,
|
|
296
|
+
introOffer: {
|
|
297
|
+
rawPrice: { monthly: 500, full: 1500 },
|
|
298
|
+
intervalUnit: 'month',
|
|
299
|
+
intervalCount: 3,
|
|
300
|
+
isOfferComplete: false,
|
|
301
|
+
formattedPrice: '$15',
|
|
302
|
+
},
|
|
303
|
+
} );
|
|
304
|
+
const result = fromPricingMetaForGridPlan( meta );
|
|
305
|
+
expect( result?.introOffer?.durationMonths ).toBe( 3 );
|
|
306
|
+
} );
|
|
307
|
+
|
|
308
|
+
it( 'returns null when originalPrice.monthly is null', () => {
|
|
309
|
+
const meta = makePricingMeta( {
|
|
310
|
+
billingPeriod: 365,
|
|
311
|
+
originalPrice: { monthly: null, full: null },
|
|
312
|
+
} );
|
|
313
|
+
expect( fromPricingMetaForGridPlan( meta ) ).toBeNull();
|
|
314
|
+
} );
|
|
315
|
+
|
|
316
|
+
it( 'returns null when billingPeriod is undefined', () => {
|
|
317
|
+
const meta = makePricingMeta( { billingPeriod: undefined } );
|
|
318
|
+
expect( fromPricingMetaForGridPlan( meta ) ).toBeNull();
|
|
319
|
+
} );
|
|
320
|
+
|
|
321
|
+
it( 'returns null for billingPeriod -1 (lifetime/unknown)', () => {
|
|
322
|
+
const meta = makePricingMeta( { billingPeriod: -1 } );
|
|
323
|
+
expect( fromPricingMetaForGridPlan( meta ) ).toBeNull();
|
|
324
|
+
} );
|
|
325
|
+
} );
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// fromVariantPriceData
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
describe( 'fromVariantPriceData', () => {
|
|
332
|
+
it( 'converts a monthly plan with no intro offer', () => {
|
|
333
|
+
const variant = makeVariant( {
|
|
334
|
+
termIntervalInMonths: 1,
|
|
335
|
+
priceInteger: 2000,
|
|
336
|
+
priceBeforeDiscounts: 2000,
|
|
337
|
+
introductoryInterval: 0,
|
|
338
|
+
} );
|
|
339
|
+
expect( fromVariantPriceData( variant ) ).toEqual( {
|
|
340
|
+
termMonths: 1,
|
|
341
|
+
regularPricePerMonth: 2000,
|
|
342
|
+
introOffer: undefined,
|
|
343
|
+
} );
|
|
344
|
+
} );
|
|
345
|
+
|
|
346
|
+
it( 'converts an annual plan with no intro offer', () => {
|
|
347
|
+
const variant = makeVariant( {
|
|
348
|
+
termIntervalInMonths: 12,
|
|
349
|
+
priceInteger: 24000,
|
|
350
|
+
priceBeforeDiscounts: 24000,
|
|
351
|
+
introductoryInterval: 0,
|
|
352
|
+
} );
|
|
353
|
+
const result = fromVariantPriceData( variant );
|
|
354
|
+
expect( result.termMonths ).toBe( 12 );
|
|
355
|
+
expect( result.regularPricePerMonth ).toBe( 2000 );
|
|
356
|
+
expect( result.introOffer ).toBeUndefined();
|
|
357
|
+
} );
|
|
358
|
+
|
|
359
|
+
it( 'converts an annual plan where the entire term is the intro (1-year intro on annual plan)', () => {
|
|
360
|
+
// priceInteger = 10000 (intro year), priceBeforeDiscounts = 24000 (regular year)
|
|
361
|
+
// introDuration = 1 × 12 = 12 months = full term
|
|
362
|
+
// nonIntroMonths = 12 - 12 = 0
|
|
363
|
+
// introPriceTotal = 10000 - 0 × 2000 = 10000
|
|
364
|
+
// introPricePerMonth = Math.round(10000 / 12) = Math.round(833.33) = 833
|
|
365
|
+
const variant = makeVariant( {
|
|
366
|
+
termIntervalInMonths: 12,
|
|
367
|
+
priceInteger: 10000,
|
|
368
|
+
priceBeforeDiscounts: 24000,
|
|
369
|
+
introductoryInterval: 1,
|
|
370
|
+
introductoryTerm: 'year',
|
|
371
|
+
} );
|
|
372
|
+
const result = fromVariantPriceData( variant );
|
|
373
|
+
expect( result.termMonths ).toBe( 12 );
|
|
374
|
+
expect( result.regularPricePerMonth ).toBe( 2000 );
|
|
375
|
+
expect( result.introOffer ).toEqual( {
|
|
376
|
+
pricePerMonth: 833,
|
|
377
|
+
durationMonths: 12,
|
|
378
|
+
isActive: true,
|
|
379
|
+
} );
|
|
380
|
+
} );
|
|
381
|
+
|
|
382
|
+
it( 'derives intro price for a biennial plan with a 1-year intro', () => {
|
|
383
|
+
// priceBeforeDiscounts = 48000 (2 × 24000 regular year)
|
|
384
|
+
// priceInteger = 34000 (intro year 10000 + regular year 24000)
|
|
385
|
+
// regularPricePerMonth = 48000 / 24 = 2000
|
|
386
|
+
// introDuration = 1 × 12 = 12 months
|
|
387
|
+
// nonIntroMonths = 24 - 12 = 12
|
|
388
|
+
// introPriceTotal = 34000 - 12 × 2000 = 34000 - 24000 = 10000
|
|
389
|
+
// introPricePerMonth = Math.round(10000 / 12) = Math.round(833.33) = 833
|
|
390
|
+
const variant = makeVariant( {
|
|
391
|
+
termIntervalInMonths: 24,
|
|
392
|
+
priceInteger: 34000,
|
|
393
|
+
priceBeforeDiscounts: 48000,
|
|
394
|
+
introductoryInterval: 1,
|
|
395
|
+
introductoryTerm: 'year',
|
|
396
|
+
} );
|
|
397
|
+
const result = fromVariantPriceData( variant );
|
|
398
|
+
expect( result.termMonths ).toBe( 24 );
|
|
399
|
+
expect( result.regularPricePerMonth ).toBe( 2000 );
|
|
400
|
+
expect( result.introOffer ).toEqual( {
|
|
401
|
+
pricePerMonth: 833,
|
|
402
|
+
durationMonths: 12,
|
|
403
|
+
isActive: true,
|
|
404
|
+
} );
|
|
405
|
+
} );
|
|
406
|
+
|
|
407
|
+
it( 'rounds regularPricePerMonth to the nearest integer for non-evenly-divisible prices', () => {
|
|
408
|
+
// 10000 / 12 = 833.333... → rounds to 833 (not 834, and not a fraction)
|
|
409
|
+
const variant = makeVariant( {
|
|
410
|
+
termIntervalInMonths: 12,
|
|
411
|
+
priceBeforeDiscounts: 10000,
|
|
412
|
+
priceInteger: 10000,
|
|
413
|
+
introductoryInterval: 0,
|
|
414
|
+
} );
|
|
415
|
+
const result = fromVariantPriceData( variant );
|
|
416
|
+
expect( result.regularPricePerMonth ).toBe( 833 );
|
|
417
|
+
expect( Number.isInteger( result.regularPricePerMonth ) ).toBe( true );
|
|
418
|
+
} );
|
|
419
|
+
|
|
420
|
+
it( 'rounds introOffer.pricePerMonth to the nearest integer for non-evenly-divisible prices', () => {
|
|
421
|
+
// Annual plan, full term is the intro period
|
|
422
|
+
// priceBeforeDiscounts = 24000 → regularPricePerMonth = 2000 (exact)
|
|
423
|
+
// priceInteger = 5000 → introPriceTotal = 5000 - 0*2000 = 5000
|
|
424
|
+
// 5000 / 12 = 416.666... → rounds to 417
|
|
425
|
+
const variant = makeVariant( {
|
|
426
|
+
termIntervalInMonths: 12,
|
|
427
|
+
priceBeforeDiscounts: 24000,
|
|
428
|
+
priceInteger: 5000,
|
|
429
|
+
introductoryInterval: 1,
|
|
430
|
+
introductoryTerm: 'year',
|
|
431
|
+
} );
|
|
432
|
+
const result = fromVariantPriceData( variant );
|
|
433
|
+
expect( result.introOffer?.pricePerMonth ).toBe( 417 );
|
|
434
|
+
expect( Number.isInteger( result.introOffer?.pricePerMonth ) ).toBe( true );
|
|
435
|
+
} );
|
|
436
|
+
|
|
437
|
+
it( 'handles a monthly intro offer (introductoryTerm: month)', () => {
|
|
438
|
+
// Monthly plan with a 1-month intro: the whole term is intro
|
|
439
|
+
// termIntervalInMonths: 12, introDuration: 1 month
|
|
440
|
+
// priceBeforeDiscounts: 24000, priceInteger: 22000
|
|
441
|
+
// regularPricePerMonth = 24000 / 12 = 2000
|
|
442
|
+
// nonIntroMonths = 12 - 1 = 11
|
|
443
|
+
// introPriceTotal = 22000 - 11 × 2000 = 22000 - 22000 = 0 — not useful
|
|
444
|
+
// Use a simpler case: termIntervalInMonths: 1, 1-month intro
|
|
445
|
+
const variant = makeVariant( {
|
|
446
|
+
termIntervalInMonths: 1,
|
|
447
|
+
priceInteger: 1000,
|
|
448
|
+
priceBeforeDiscounts: 2000,
|
|
449
|
+
introductoryInterval: 1,
|
|
450
|
+
introductoryTerm: 'month',
|
|
451
|
+
} );
|
|
452
|
+
const result = fromVariantPriceData( variant );
|
|
453
|
+
expect( result.introOffer?.durationMonths ).toBe( 1 );
|
|
454
|
+
// nonIntroMonths = 1 - 1 = 0; introPriceTotal = 1000 - 0 = 1000
|
|
455
|
+
expect( result.introOffer?.pricePerMonth ).toBe( 1000 );
|
|
456
|
+
} );
|
|
457
|
+
|
|
458
|
+
it( 'produces no introOffer when introductoryInterval is 0', () => {
|
|
459
|
+
const variant = makeVariant( {
|
|
460
|
+
priceInteger: 24000,
|
|
461
|
+
priceBeforeDiscounts: 24000,
|
|
462
|
+
introductoryInterval: 0,
|
|
463
|
+
} );
|
|
464
|
+
expect( fromVariantPriceData( variant ).introOffer ).toBeUndefined();
|
|
465
|
+
} );
|
|
466
|
+
|
|
467
|
+
it( 'produces no introOffer when priceInteger equals priceBeforeDiscounts (no actual discount)', () => {
|
|
468
|
+
// introductoryInterval is set but prices are equal — indicates intro not applied
|
|
469
|
+
const variant = makeVariant( {
|
|
470
|
+
termIntervalInMonths: 12,
|
|
471
|
+
priceInteger: 24000,
|
|
472
|
+
priceBeforeDiscounts: 24000,
|
|
473
|
+
introductoryInterval: 1,
|
|
474
|
+
introductoryTerm: 'year',
|
|
475
|
+
} );
|
|
476
|
+
expect( fromVariantPriceData( variant ).introOffer ).toBeUndefined();
|
|
477
|
+
} );
|
|
478
|
+
|
|
479
|
+
it( 'handles a multi-month intro on a monthly plan (introDurationMonths > termMonths)', () => {
|
|
480
|
+
// Monthly plan ($20/month), 3-month intro at $5/month.
|
|
481
|
+
// termMonths=1, introDurationMonths=3 → whole term is within the intro period.
|
|
482
|
+
// nonIntroMonths = max(0, 1 - 3) = 0
|
|
483
|
+
// introPriceTotal = 500 - 0 = 500
|
|
484
|
+
// introPricePerMonth = round(500 / min(3, 1)) = round(500 / 1) = 500
|
|
485
|
+
const variant = makeVariant( {
|
|
486
|
+
termIntervalInMonths: 1,
|
|
487
|
+
priceBeforeDiscounts: 2000,
|
|
488
|
+
priceInteger: 500,
|
|
489
|
+
introductoryInterval: 3,
|
|
490
|
+
introductoryTerm: 'month',
|
|
491
|
+
} );
|
|
492
|
+
const result = fromVariantPriceData( variant );
|
|
493
|
+
expect( result.introOffer?.pricePerMonth ).toBe( 500 );
|
|
494
|
+
expect( result.introOffer?.durationMonths ).toBe( 3 );
|
|
495
|
+
} );
|
|
496
|
+
|
|
497
|
+
it( 'handles a multi-year intro on an annual plan (introDurationMonths > termMonths)', () => {
|
|
498
|
+
// Annual plan ($200/year), 3-year intro at $100/year.
|
|
499
|
+
// termMonths=12, introDurationMonths=36 → whole term is within the intro period.
|
|
500
|
+
// regularPricePerMonth = round(20000 / 12) = 1667
|
|
501
|
+
// nonIntroMonths = max(0, 12 - 36) = 0
|
|
502
|
+
// introPriceTotal = 10000 - 0 = 10000
|
|
503
|
+
// introPricePerMonth = round(10000 / min(36, 12)) = round(10000 / 12) = 833
|
|
504
|
+
const variant = makeVariant( {
|
|
505
|
+
termIntervalInMonths: 12,
|
|
506
|
+
priceBeforeDiscounts: 20000,
|
|
507
|
+
priceInteger: 10000,
|
|
508
|
+
introductoryInterval: 3,
|
|
509
|
+
introductoryTerm: 'year',
|
|
510
|
+
} );
|
|
511
|
+
const result = fromVariantPriceData( variant );
|
|
512
|
+
expect( result.introOffer?.pricePerMonth ).toBe( 833 );
|
|
513
|
+
expect( result.introOffer?.durationMonths ).toBe( 36 );
|
|
514
|
+
} );
|
|
515
|
+
|
|
516
|
+
it( 'produces no introOffer when introPriceTotal is non-positive (inconsistent data)', () => {
|
|
517
|
+
// Annual plan with a 3-month intro, but priceInteger is too low to be consistent:
|
|
518
|
+
// regularPricePerMonth = round(24000 / 12) = 2000
|
|
519
|
+
// nonIntroMonths = 12 - 3 = 9
|
|
520
|
+
// introPriceTotal = 1000 - 9 × 2000 = 1000 - 18000 = -17000 → bail out
|
|
521
|
+
const variant = makeVariant( {
|
|
522
|
+
termIntervalInMonths: 12,
|
|
523
|
+
priceBeforeDiscounts: 24000,
|
|
524
|
+
priceInteger: 1000,
|
|
525
|
+
introductoryInterval: 3,
|
|
526
|
+
introductoryTerm: 'month',
|
|
527
|
+
} );
|
|
528
|
+
expect( fromVariantPriceData( variant ).introOffer ).toBeUndefined();
|
|
529
|
+
} );
|
|
530
|
+
|
|
531
|
+
it( 'does not set discountedPricePerMonth', () => {
|
|
532
|
+
const variant = makeVariant();
|
|
533
|
+
const result = fromVariantPriceData( variant );
|
|
534
|
+
expect( result.discountedPricePerMonth ).toBeUndefined();
|
|
535
|
+
} );
|
|
536
|
+
} );
|
|
537
|
+
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
// Integration: adapters + getPlanPriceForDuration + calculateDiscountPercentage
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
describe( 'integration — fromPricingMetaForGridPlan → getPlanPriceForDuration → calculateDiscountPercentage', () => {
|
|
543
|
+
it( 'calculates the annual vs monthly discount (no intro offers)', () => {
|
|
544
|
+
const monthlyMeta = makePricingMeta( {
|
|
545
|
+
billingPeriod: 31,
|
|
546
|
+
originalPrice: { monthly: 2000, full: 2000 },
|
|
547
|
+
} );
|
|
548
|
+
const yearlyMeta = makePricingMeta( {
|
|
549
|
+
billingPeriod: 365,
|
|
550
|
+
originalPrice: { monthly: 1500, full: 18000 },
|
|
551
|
+
} );
|
|
552
|
+
|
|
553
|
+
const monthlyInfo = fromPricingMetaForGridPlan( monthlyMeta )!;
|
|
554
|
+
const yearlyInfo = fromPricingMetaForGridPlan( yearlyMeta )!;
|
|
555
|
+
|
|
556
|
+
// Compare monthly-equivalent prices for 12 months
|
|
557
|
+
const monthlyEquivPerMonth =
|
|
558
|
+
getPlanPriceForDuration( monthlyInfo, 12, { useIntroOffer: false } ) / 12;
|
|
559
|
+
const yearlyEquivPerMonth =
|
|
560
|
+
getPlanPriceForDuration( yearlyInfo, 12, { useIntroOffer: false } ) / 12;
|
|
561
|
+
|
|
562
|
+
// (2000 - 1500) / 2000 × 100 = 25
|
|
563
|
+
expect( calculateDiscountPercentage( monthlyEquivPerMonth, yearlyEquivPerMonth ) ).toBe( 25 );
|
|
564
|
+
} );
|
|
565
|
+
} );
|
|
566
|
+
|
|
567
|
+
describe( 'integration — fromVariantPriceData → getPlanPriceForDuration → calculateDiscountPercentage', () => {
|
|
568
|
+
it( 'calculates the upsell discount from annual to biennial (no intros)', () => {
|
|
569
|
+
const annualVariant = makeVariant( {
|
|
570
|
+
termIntervalInMonths: 12,
|
|
571
|
+
priceInteger: 24000,
|
|
572
|
+
priceBeforeDiscounts: 24000,
|
|
573
|
+
introductoryInterval: 0,
|
|
574
|
+
} );
|
|
575
|
+
const biennialVariant = makeVariant( {
|
|
576
|
+
termIntervalInMonths: 24,
|
|
577
|
+
priceInteger: 40000,
|
|
578
|
+
priceBeforeDiscounts: 40000,
|
|
579
|
+
introductoryInterval: 0,
|
|
580
|
+
} );
|
|
581
|
+
|
|
582
|
+
const annualInfo = fromVariantPriceData( annualVariant );
|
|
583
|
+
const biennialInfo = fromVariantPriceData( biennialVariant );
|
|
584
|
+
|
|
585
|
+
// Normalise both to per-month cost over the biennial period (24 months)
|
|
586
|
+
const refPerMonth = getPlanPriceForDuration( annualInfo, 24 ) / 24;
|
|
587
|
+
const cheaperPerMonth = getPlanPriceForDuration( biennialInfo, 24 ) / 24;
|
|
588
|
+
|
|
589
|
+
// annualInfo per month: 24000/12 = 2000; over 24 months: 48000 → 2000/mo
|
|
590
|
+
// biennialInfo per month: 40000/24 ≈ 1666.67/mo
|
|
591
|
+
// discount = floor((2000 - 1666.67) / 2000 × 100) = floor(16.67) = 16
|
|
592
|
+
expect( calculateDiscountPercentage( refPerMonth, cheaperPerMonth ) ).toBe( 16 );
|
|
593
|
+
} );
|
|
594
|
+
} );
|
package/src/types.ts
CHANGED
|
@@ -18,6 +18,14 @@ export type TransformedFeatureObject = FeatureObject & {
|
|
|
18
18
|
availableForCurrentPlan: boolean;
|
|
19
19
|
availableOnlyForAnnualPlans: boolean;
|
|
20
20
|
isHighlighted?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* When true, extra bottom margin on the last feature row for pricing experiment variants.
|
|
23
|
+
*/
|
|
24
|
+
isExperimentLastFeature?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Badge text to display after the feature title (e.g. pricing differentiators pills).
|
|
27
|
+
*/
|
|
28
|
+
badgeText?: TranslateResult;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
export interface PlanFeaturesForGridPlan {
|
|
@@ -45,6 +53,7 @@ export interface GridPlan {
|
|
|
45
53
|
product_slug: string;
|
|
46
54
|
} | null;
|
|
47
55
|
highlightLabel?: React.ReactNode | null;
|
|
56
|
+
titleBadge?: React.ReactNode | null;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
/***********************
|
|
@@ -55,6 +64,7 @@ export type GridSize = 'small' | 'smedium' | 'medium' | 'large' | 'xlarge';
|
|
|
55
64
|
|
|
56
65
|
export type PlansIntent =
|
|
57
66
|
| 'plans-affiliate'
|
|
67
|
+
| 'plans-ai-assembler-free-trial'
|
|
58
68
|
| 'plans-blog-onboarding'
|
|
59
69
|
| 'plans-newsletter'
|
|
60
70
|
| 'plans-new-hosted-site'
|
|
@@ -74,6 +84,15 @@ export type PlansIntent =
|
|
|
74
84
|
| 'plans-guided-segment-nonprofit'
|
|
75
85
|
| 'plans-guided-segment-consumer-or-business'
|
|
76
86
|
| 'plans-site-selected-legacy'
|
|
87
|
+
| 'plans-playground'
|
|
88
|
+
| 'plans-playground-premium' // This plan intent is currently not utilized but will be soon
|
|
89
|
+
| 'plans-upgrade'
|
|
90
|
+
| 'plans-upgrade-or-downgrade'
|
|
91
|
+
| 'plans-wordpress-hosting'
|
|
92
|
+
| 'plans-website-builder'
|
|
93
|
+
| 'plans-woo-hosted'
|
|
94
|
+
| 'plans-woo-hosting-solutions'
|
|
95
|
+
| 'plans-migration'
|
|
77
96
|
| 'default';
|
|
78
97
|
|
|
79
98
|
export interface PlanActionOverrides {
|
|
@@ -135,6 +154,8 @@ export interface ComparisonGridProps extends CommonGridProps {
|
|
|
135
154
|
// Value of the `?plan=` query param, so we can highlight a given plan.
|
|
136
155
|
selectedPlan?: string;
|
|
137
156
|
intervalType: SupportedUrlFriendlyTermType;
|
|
157
|
+
/** Called when the number of visible plans in the grid changes (e.g. for narrowing the container). */
|
|
158
|
+
onVisiblePlansCountChange?: ( count: number ) => void;
|
|
138
159
|
}
|
|
139
160
|
|
|
140
161
|
export type UseActionCallback = ( {
|
|
@@ -152,6 +173,7 @@ export type UseActionCallback = ( {
|
|
|
152
173
|
export interface GridAction {
|
|
153
174
|
primary: {
|
|
154
175
|
text: TranslateResult;
|
|
176
|
+
ariaLabel?: TranslateResult;
|
|
155
177
|
callback: () => Promise< void > | void;
|
|
156
178
|
// TODO: It's not clear if status is ever actually set to 'blocked'. Investigate and remove if not.
|
|
157
179
|
status?: 'disabled' | 'blocked' | 'enabled';
|
|
@@ -172,6 +194,8 @@ export type UseAction = ( {
|
|
|
172
194
|
planTitle,
|
|
173
195
|
priceString,
|
|
174
196
|
selectedStorageAddOn,
|
|
197
|
+
pricing,
|
|
198
|
+
isMonthlyPlan,
|
|
175
199
|
}: {
|
|
176
200
|
availableForPurchase?: boolean;
|
|
177
201
|
billingPeriod?: PlanPricing[ 'billPeriod' ];
|
|
@@ -184,6 +208,8 @@ export type UseAction = ( {
|
|
|
184
208
|
planTitle?: TranslateResult;
|
|
185
209
|
priceString?: string;
|
|
186
210
|
selectedStorageAddOn?: AddOns.AddOnMeta | null;
|
|
211
|
+
pricing?: Plans.PricingMetaForGridPlan | null;
|
|
212
|
+
isMonthlyPlan?: boolean;
|
|
187
213
|
} ) => GridAction;
|
|
188
214
|
|
|
189
215
|
export type GridContextProps = {
|
|
@@ -239,6 +265,21 @@ export type GridContextProps = {
|
|
|
239
265
|
* calculating prices.
|
|
240
266
|
*/
|
|
241
267
|
reflectStorageSelectionInPlanPrices?: boolean;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Enable simplified billing description
|
|
271
|
+
*/
|
|
272
|
+
showSimplifiedBillingDescription?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* If, and how to present increased renewal pricing (null or the assigned variant name)
|
|
275
|
+
*/
|
|
276
|
+
showBillingDescriptionForIncreasedRenewalPrice?: string | null;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* When true, the user is in the rolled-out pricing differentiation cohort.
|
|
280
|
+
* Used to display cohort-specific feature titles in the comparison grid.
|
|
281
|
+
*/
|
|
282
|
+
isExperimentVariant?: boolean;
|
|
242
283
|
};
|
|
243
284
|
|
|
244
285
|
export type ComparisonGridExternalProps = Omit<
|